Procházet soubory s mezerami v názvech? [duplicate]

Tato otázka již má odpovědi zde :

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:

  1. Ve výchozím nastavení rozděluje prostředí výstup příkazu na mezery, karty a nové řádky
  2. Názvy souborů mohou obsahovat zástupné znaky, které by se rozšířilo
  3. 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í while v 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, while a read pokud zpracováváte find celý, 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ě:

  1. Proveďte příkaz find, uchopte jeho výstup.
  2. Rozdělte výstup find na samostatná slova. Libovolný znak mezery je oddělovačem slov.
  3. 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.

  1. Příkaz find vrací ./foo* bar.csv.
  2. Shell tento řetězec rozdělí v prostoru, produkující dvě slova: ./foo* a bar.csv.
  3. Protože ./foo* obsahuje globující metaznak, je rozšířen na seznam odpovídajících souborů: ./foo 1.txt a ./foo 2.txt.
  4. Proto se smyčka for provádí postupně s ./foo 1.txt, ./foo 2.txt a bar.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žít find -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 v find -exec není 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 část echo může být provedena -print. -okdir je 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žit read do 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í sh a 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 "{}" \; s time find -type f -print0 | xargs -0 -I stuff cat stuff. Verze xargs byla 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 kombinace find s jinými nástroji nesprávná. -print0 a -0 jsou 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žívat xargs tí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). find tam 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ázvem Hello World.csv, bude file nastaven na ./Hello a poté na World.csv. Citace $file nepomůž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ázvem Hello World.csv, file bude nastaven na ./Hello a poté na World.csv. Citace $file nepomůž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 -l výstup najednou . Je skutečně ekvivalentní s echo "CHECKSTR `ls -l /root/somedir`".

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *