Komentáře
- Nesouhlasím s tím, že by šlo o duplikát. Přijatá odpověď odpovídá na to, jak procházet názvy souborů s mezerami; to nemá nic společného s " proč je smyčka přes find ' s špatným postupem výstupu ". Našel jsem tuto otázku (ne druhou), protože potřebuji smyčku přes názvy souborů s mezerami, jako v: pro soubor v $ LIST_OF_FILES; do … kde $ LIST_OF_FILES není výstupem z find; ' obsahuje pouze seznam názvů souborů (oddělených novými řádky).
- @CarloWood – názvy souborů mohou obsahovat nové řádky, takže vaše otázka je zcela jedinečná: opakování seznam názvů souborů, které mohou obsahovat mezery, ale ne nové řádky. Myslím, že ' budete muset použít techniku IFS, abyste naznačili, že ke zlomu dojde v ' \ n '
- @ Diagonwoah, nikdy jsem si neuvědomil, že názvy souborů mohou obsahovat nové řádky. Používám většinou (pouze) linux / UNIX a tam jsou dokonce mezery vzácné; Rozhodně jsem nikdy za celý svůj život neviděl použití nových řádků: str. Mohli by také zakázat, aby imho.
- @CarloWood – názvy souborů skončily nulou (' \ 0 ' , stejné jako ' '). Cokoli jiného je přijatelné.
- @CarloWood Musíte si uvědomit, že lidé hlasují jako první a čtou jako druzí …
Odpovědět
Krátká odpověď (nejblíže vaší odpovědi, ale zvládá mezery)
OIFS="$IFS" IFS=$"\n" for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS"
Lepší odpověď (zpracovává také zástupné znaky a nové řádky v názvech souborů)
find . -type f -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
Nejlepší odpověď (na základě Gilles “ answer )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Nebo ještě lépe, abyste se vyhnuli spuštění sh na soubor:
find . -type f -name "*.csv" -exec sh -c " for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done " exec-sh {} +
Dlouhá odpověď
Máte tři problémy:
- Ve výchozím nastavení rozděluje prostředí výstup příkazu na mezery, karty a nové řádky
- Názvy souborů mohou obsahovat zástupné znaky, které by se rozšířilo
- Co když existuje adresář, jehož název končí na
*.csv?
1. Rozdělení pouze na nové řádky
Chcete-li zjistit, na co chcete nastavit file, musí shell převzít výstup z find a nějak to interpretovat, jinak by file byl jen celý výstup find .
Shell načte proměnnou IFS, která je ve výchozím nastavení nastavena na <space><tab><newline>.
Poté se podívá na každý znak na výstupu find. Jakmile uvidí jakýkoli znak, který je v IFS, myslí si, že označuje konec názvu souboru, takže nastaví file na jakékoli znaky, které dosud viděla, a spustí smyčku. Poté začne tam, kde přestala, aby získala název dalšího souboru, a spustí další smyčku atd., dokud nedosáhne konce výstupu.
Účinně to tedy dělá:
for file in "zquery" "-" "abc" ...
Chcete-li, aby se vstup rozdělil pouze na nové řádky, musíte udělat
IFS=$"\n"
před vaším for ... find příkazem.
Tím se nastaví IFS na a jeden nový řádek, takže se rozdělí pouze na nové řádky, a nikoli také na mezery a karty.
Pokud používáte sh nebo dash místo ksh93, bash nebo zsh musíte napsat IFS=$"\n" místo toho takto:
IFS=" "
To je pravděpodobně dost aby váš skript fungoval, ale pokud máte zájem o správné zvládnutí některých dalších rohových případů, přečtěte si …
2. Rozbalení $file bez zástupných znaků
Uvnitř smyčky, kde provádíte
diff $file /some/other/path/$file
shell se pokusí rozbalit $file (znovu!).
Může obsahovat mezery, ale protože jsme již nastavili IFS výše, zde to nebude problém.
Mohl by však také obsahovat zástupné znaky, například * nebo ?, což by vedlo k nepředvídatelnému chování. (Děkujeme Gillesovi, že na to upozornil.)
Chcete-li, aby prostředí nerozšiřovalo zástupné znaky, vložte proměnnou do uvozovek, např.
diff "$file" "/some/other/path/$file"
Stejný problém by nás také mohl kousnout
for file in `find . -name "*.csv"`
Například pokud jste měli tyto tři soubory
file1.csv file2.csv *.csv
(velmi nepravděpodobné, ale stále možné)
Bylo by to, jako byste běhali
for file in file1.csv file2.csv *.csv
který bude rozšířen na
for file in file1.csv file2.csv *.csv file1.csv file2.csv
způsobující file1.csv a file2.csv ke zpracování dvakrát.
Místo toho musíme udělat
find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
read čte řádky ze standardního vstupu, rozděluje řádky na slova podle IFS a ukládá je do jmen proměnných, které určíte.
Tady to říkáme nerozdělit řádek na slova a uložit řádek do $file.
Všimněte si také, že se změnilo na read line </dev/tty.
Je to proto, že uvnitř smyčky pochází standardní vstup z find prostřednictvím kanálu.
Pokud bychom právě udělali read, spotřebovalo by to část nebo celý název souboru a některé soubory by byly přeskočeny .
/dev/tty je terminál, ze kterého uživatel spouští skript. Všimněte si, že to způsobí chybu, pokud je skript spuštěn přes cron, ale předpokládám, že to v tomto případě není důležité.
Potom, co když název souboru obsahuje nové řádky?
Zvládneme to změnou -print na -print0 a použitím read -d "" na konci pipeline:
find . -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done
Díky tomu find umístí na konec každého názvu souboru nulový bajt. Nulové bajty jsou jediné znaky, které nejsou povoleny v názvech souborů, takže by to mělo zpracovávat všechny možné názvy souborů, bez ohledu na to, jak divné.
Chcete-li získat název souboru na druhé straně, použijeme IFS= read -r -d "".
Tam, kde jsme použili read výše, jsme použili výchozí oddělovač řádků nového řádku, nyní však find používá null jako oddělovač řádků. V bash nemůžete předat znak NUL v argumentu příkazu (i vestavěnému), ale bash chápe -d "" ve smyslu s oddělením NUL . Takže pomocí -d "" vytvoříme read použijte stejný oddělovač řádků jako find. Všimněte si, že -d $"\0" mimochodem také funguje, protože bash nepodporující NUL bajty s ním zachází jako s prázdným řetězcem.
Abych byl správný, přidáme také -r, který říká, že v názvy souborů zvlášť. Například bez -r jsou \<newline> odstraněny a \n je převeden na n.
Přenosnější způsob psaní, který nevyžaduje bash nebo zsh nebo zapamatování všech výše uvedených pravidel o nulových bajtech (opět díky Gillesovi):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Přeskakování adresářů, jejichž jména končí na .csv
find . -name "*.csv"
budou také odpovídat adresářům, které se nazývají something.csv.
Chcete-li se tomu vyhnout, přidejte -type f do příkazu find.
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Jak zdůrazňuje glenn jackman , v obou těchto příkladech jsou příkazy k provedení pro každý soubor běží v subshell, takže pokud změníte nějaké proměnné uvnitř smyčky, budou zapomenuty.
Pokud potřebujete nastavit proměnné a nechat je stále nastavovat na konci smyčky jej můžete přepsat a použít takovou substituci procesu:
i=0 while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name "*.csv" -print0) echo "$i files processed"
Všimněte si, že pokud to zkopírujete a vložíte na příkazový řádek , read line spotřebuje echo "$i files processed", takže příkaz nebude spuštěn.
Abyste tomu zabránili, musíte mohl odstranit read line </dev/tty a poslat výsledek na pager jako less.
POZNÁMKY
Odstranil jsem středníky (;) uvnitř smyčka. Pokud je chcete, můžete je vrátit, ale nejsou potřeba.
V dnešní době je $(command) častější než `command`. Je to hlavně proto, že je snazší psát $(command1 $(command2)) než `command1 \`command2\``.
read char opravdu nečte postavu.Přečte celý řádek, takže jsem to změnil na read line.
Komentáře
- uvedení
whilev potrubí může vytvářet problémy s vytvořeným subshellem (například proměnné v bloku smyčky nejsou viditelné po dokončení příkazu). S bash bych použil přesměrování vstupu a náhradu procesu:while read -r -d $'\0' file; do ...; done < <(find ... -print0) - Jistě, nebo pomocí heredoc:
while read; do; done <<EOF "$(find)" EOF. Není to však tak snadné číst. - @glenn jackman: Právě jsem se pokusil přidat další vysvětlení. Dokázal jsem to jen vylepšit nebo zhoršit?
- Nepotřebujete '
IFS, -print0, whileareadpokud zpracovávátefindcelý, jak je uvedeno níže v mém řešení. - Vaše první řešení si poradí s jakýmkoli znakem kromě nového řádku pokud také vypnete globování pomocí
set -f.
Odpovědět
Tento skript selže, pokud libovolný název souboru obsahuje mezery nebo znaky globbingu \[?*. Příkaz find vypíše na každý řádek jeden název souboru. Substituce příkazu `find …` je pak shellem vyhodnocena následovně:
- Proveďte příkaz
find, uchopte jeho výstup. - Rozdělte výstup
findna samostatná slova. Libovolný znak mezery je oddělovačem slov. - Pokud jde o vzor globování, rozbalte jej pro každý seznam, který odpovídá.
Například Předpokládejme, že v aktuálním adresáři jsou tři soubory, které se nazývají `foo* bar.csv, foo 1.txt a foo 2.txt.
- Příkaz
findvrací./foo* bar.csv. - Shell tento řetězec rozdělí v prostoru, produkující dvě slova:
./foo*abar.csv. - Protože
./foo*obsahuje globující metaznak, je rozšířen na seznam odpovídajících souborů:./foo 1.txta./foo 2.txt. - Proto se smyčka
forprovádí postupně s./foo 1.txt,./foo 2.txtabar.csv.
Většině problémů v této fázi se můžete vyhnout zmírněním rozdělení slov a otočením vypíná se globování. Chcete-li zmírnit dělení slov, nastavte proměnnou IFS na jeden znak nového řádku; tímto způsobem bude výstup find rozdělen pouze na nové řádky a mezery zůstanou. Globování vypnete spuštěním set -f. Pak tato část kódu bude fungovat, dokud žádný název souboru nebude obsahovat znak nového řádku.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Toto není součástí vašeho problému, ale já doporučujeme použít $(…) nad `…`. Mají stejný význam, ale verze backquote má zvláštní pravidla pro citování.)
Níže je uveden další problém: diff $file /some/other/path/$file by měl být
diff "$file" "/some/other/path/$file"
Jinak by hodnota $file je rozdělena na slova a se slovy se zachází jako s globovými vzory, jako u výše uvedeného příkazu substitutio. Pokud si musíte pamatovat jednu věc ohledně programování prostředí, pamatujte na toto: vždy používejte uvozovky kolem proměnných rozšíření ($foo) a substituce příkazů ( $(bar)) , pokud nevíte, že se chcete rozdělit. (Nahoře jsme věděli, že chceme rozdělit výstup find na řádky.)
Spolehlivý způsob volání find říká mu, aby spustil příkaz pro každý nalezený soubor:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
V tomto případě je dalším přístupem porovnání těchto dvou adresářů, i když musíte výslovně vyloučit všechny „nudné“ soubory.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Komentáře
- I ' zapomněl jsem na zástupné znaky jako další důvod, proč správně citovat. Dík! 🙂
- namísto
find -exec sh -c 'cmd 1; cmd 2' ";"byste měli použítfind -exec cmd 1 {} ";" -exec cmd 2 {} ";", protože shell musí maskovat parametry, ale find doesn ' t. Ve zvláštním případě zde echo " $ 0 " nemusí být ' t část skriptu, stačí přidat -print za';'. Nezahrnuli jste ' otázku k pokračování, ale i to lze provést hledáním, jak je ukázáno níže v mé duši. 😉 - @userunknown: Použití
{}jako podřetězce parametru vfind -execnení přenosné, to je ' důvod, proč je shell nutný.Nerozumím ' tomu, co máte na mysli „shell potřebuje maskovat parametry“; pokud ' jde o citování, moje řešení je správně citováno. Máte ' pravdu, že částechomůže být provedena-print.-okdirje poměrně nedávné rozšíření hledání GNU, ' není k dispozici všude. Nezahrnul jsem ' čekání na pokračování, protože se domnívám, že extrémně špatné uživatelské rozhraní a žadatel mohou snadno vložitreaddo fragmentu shellu, pokud chce. - Citace je formou maskování, že? ' že? Nerozumím ' vaší poznámce o tom, co je přenosné a co ne. Váš příklad (druhý zdola) používá -exec k vyvolání
sha používá{}– takže kde je můj příklad (kromě -okdir) méně přenosný?find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print - „Maskování“ není ' běžnou terminologií v literatuře prostředí, takže ' Budu muset vysvětlit, co máte na mysli, pokud chcete, aby vám někdo rozuměl. Můj příklad používá
{}pouze jednou a v samostatném argumentu; ostatní případy (použité dvakrát nebo jako podřetězec) nejsou přenosné. „Přenosný“ znamená, že ' funguje na všech unixových systémech; dobrým vodítkem je specifikace POSIX / Single Unix .
odpověď
Jsem překvapen, že nevidím readarray. Je to velmi snadné při použití v kombinaci s <<< operator:
$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words|
Použití konstrukce <<<"$expansion" také umožňuje rozdělit proměnné obsahující nové řádky do polí, jako :
$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset
readarray je v Bashi už léta, takže by to měl být pravděpodobně kanonický způsob toto v Bash.
Odpověď
Afaik find má vše, co potřebujete.
find . -okdir diff {} /some/other/path/{} ";"
find se stará o bezpečné volání programů. -okdir vás vyzve před rozdílem (jste si jisti ano / ne).
Není zapojen žádný shell, žádné globusy, vtipálci, pi, pa, po.
Jako vedlejší přítel: Pokud kombinujete find s for / while / do / xargs, ve většině případů y děláš to špatně. 🙂
Komentáře
- Děkujeme za odpověď. Proč to děláte špatně, když kombinujete find s for / while / do / xargs?
- Find již iteruje přes podmnožinu souborů. Většina lidí, kteří se objeví s otázkami, mohou použít jednu z akcí (-ok (dir) -exec (dir), -delete) v kombinaci s "; " nebo + (později pro paralelní vyvolání). Hlavním důvodem je to, že nemusíte ' manipulovat s parametry souboru a maskovat je pro shell. Není to důležité: Nepotřebujete ' t nové procesy pořád, méně paměti, více rychlosti. kratší program.
- Tady není, abyste rozdrtili svého ducha, ale porovnejte:
time find -type f -exec cat "{}" \;stime find -type f -print0 | xargs -0 -I stuff cat stuff. Verzexargsbyla při zpracování 10 000 prázdných souborů rychlejší o 11 sekund. Buďte opatrní, když tvrdíte, že ve většině případů je kombinacefinds jinými nástroji nesprávná.-print0a-0jsou určeny k řešení mezer v názvech souborů pomocí nulového bajtu jako oddělovače položek místo mezery. - @JonathanKomar: Vaše komando find / exec trvalo v mém systému s 10 000 soubory 11,7 s, verze xargs 9.7 s,
time find -type f -exec cat {} +jak bylo navrženo v mém předchozím komentáři, trvalo 0,1 s. Všimněte si rozdílu mezi ", který je špatný " a " vy ' dělá to špatně ", zvláště když je zdoben smajlíkem. Udělali jste například špatně? 😉 BTW, mezery v názvu souboru nejsou problémem pro výše uvedený příkaz a obecně se nacházejí. Nákladní kultovní programátor? A mimochodem, kombinace find s jinými nástroji je v pořádku, jen xargs je většinou superflous. - @userunknown Vysvětlil jsem, jak můj kód pracuje s mezerami pro potomky (vzdělávání budoucích diváků), a byl to neznamená, že váš kód není.
+pro paralelní volání je velmi rychlý, jak jste zmínili. Neřekl bych programátor kultu nákladu, protože tato schopnost používatxargstímto způsobem se při mnoha příležitostech hodí. Souhlasím více s filozofií Unixu: udělejte jednu věc a udělejte to dobře (k dokončení práce používejte programy samostatně nebo v kombinaci).findtam kráčí po jemné čáře.
Odpověď
Procházejte libovolné soubory ( jakýkoli speciální znak) s zcela bezpečné hledání (viz dokumentace):
exec 9< <( find "$absolute_dir_path" -type f -print0 ) while IFS= read -r -d "" -u 9 do file_path="$(readlink -fn -- "$REPLY"; echo x)" file_path="${file_path%x}" echo "START${file_path}END" done
Komentáře
- Děkujeme za zmínku
-d ''. Neuvědomil jsem si ', že$'\0'byl stejný jako'', ale zdá se, že být. Dobré řešení. - Líbí se mi oddělení plateb od find a while, díky.
Odpověď
Jsem překvapen, že zde zatím nikdo nezmínil zřejmé zsh řešení:
for file (**/*.csv(ND.)) { do-something-with $file }
((D) zahrnout také skryté soubory, (N) vyhnout se chybě, pokud neexistuje shoda, (.) omezit na běžné soubory.)
bash4.3 a výše to nyní podporuje také částečně:
shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done
Odpověď
Názvy souborů s mezerami vypadají jako více jmen na příkazovém řádku, pokud “ není citován. Pokud má váš soubor název „Hello World.txt“, řádek rozdílu se rozšíří na:
diff Hello World.txt /some/other/path/Hello World.txt
, který vypadá jako čtyři názvy souborů. Stačí vložit uvozovky kolem argumentů:
diff "$file" "/some/other/path/$file"
Komentáře
- To pomáhá, ale ne ' nevyřeší můj problém. Stále vidím případy, kdy je soubor rozdělen do několika tokenů.
- Tato odpověď je zavádějící. Problém je v příkazu
for file in `find . -name "*.csv"`. Pokud existuje soubor s názvemHello World.csv, budefilenastaven na./Helloa poté naWorld.csv. Citace$filenepomůže '
odpověď
Dvojitá citace je váš přítel.
diff "$file" "/some/other/path/$file"
Jinak bude obsah proměnné rozdělen na slova.
Komentáře
- Toto je zavádějící. Problémem je příkaz
for file in `find . -name "*.csv"`. Pokud existuje soubor s názvemHello World.csv,filebude nastaven na./Helloa poté naWorld.csv. Citace$filenepomůže '
odpověď
S bash4 můžete také použít integrovanou funkci mapfile k nastavení pole obsahujícího každý řádek a iteraci na tomto poli.
$ tree . ├── a │ ├── a 1 │ └── a 2 ├── b │ ├── b 1 │ └── b 2 └── c ├── c 1 └── c 2 3 directories, 6 files $ mapfile -t files < <(find -type f) $ for file in "${files[@]}"; do > echo "file: $file" > done file: ./a/a 2 file: ./a/a 1 file: ./b/b 2 file: ./b/b 1 file: ./c/c 2 file: ./c/c 1
Odpověď
Mezerám v hodnotách se lze vyhnout jednoduchou konstrukcí smyčky
for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done
ls -l root / somedir c získá můj soubor s mezerami
Výstup nad mým souborem s mezerami
aby se tomuto výstupu vyhnul, jednoduché řešení (všimněte si uvozovek)
for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done
výstup mého souboru s mezerami
vyzkoušeno na bash
komentáře
- „Looping through files “- to je to, co říká otázka. Vaše řešení odešle celý
ls -lvýstup najednou . Je skutečně ekvivalentní secho "CHECKSTR `ls -l /root/somedir`".