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í
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
aread
pokud zpracovávátefind
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ě:
- Proveďte příkaz
find
, uchopte jeho výstup. - Rozdělte výstup
find
na 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
find
vrací./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.txt
a./foo 2.txt
. - Proto se smyčka
for
provádí postupně s./foo 1.txt
,./foo 2.txt
abar.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 -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 částecho
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žitread
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 "{}" \;
stime find -type f -print0 | xargs -0 -I stuff cat stuff
. Verzexargs
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 kombinacefind
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žívatxargs
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ázvemHello World.csv
, budefile
nastaven na./Hello
a poté naWorld.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ázvemHello World.csv
,file
bude nastaven na./Hello
a poté naWorld.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í secho "CHECKSTR `ls -l /root/somedir`"
.