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`".