(8 odpovědí)
Uzavřeno před 3 lety.
Napsal jsem následující skript, abych porovnal výstupy dvou adresářů se všemi stejnými soubory jako takovými:
#!/bin/bash
for file in `find . -name "*.csv"`
do
echo "file = $file";
diff $file /some/other/path/$file;
read char;
done
Vím, že existují i jiné způsoby, jak toho dosáhnout. Je však zvláštní, že tento skript selže, když soubory obsahují mezery. Jak se s tím mohu vypořádat?
Příklad výstupu find:
./zQuery - abc - Do Not Prompt for Date.csv
Přijatá odpověď:
Krátká odpověď (nejbližší vaší odpovědi, ale zpracovává 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ěď (zvládá 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ě Gillesovy odpovědi)
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í jednoho 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 shell výstup příkazu na mezery, tabulátory a nové řádky
- Názvy souborů mohou obsahovat zástupné znaky, které by se rozšířily
- Co když existuje adresář, jehož název končí na
*.csv
?
1. Rozdělení pouze na nové řádky
Chcete-li zjistit, co nastavit file
shell musí převzít výstup z find
a nějak to interpretovat, jinak file
by byl pouze celý výstup find
.
Shell čte IFS
proměnná, která je nastavena na <space><tab><newline>
ve výchozím nastavení.
Poté se podívá na každý znak ve výstupu find
. Jakmile uvidí jakýkoli znak, který je v IFS
, myslí si, že to označuje konec názvu souboru, takže nastaví file
na jakékoli znaky, které dosud viděl, a spustí smyčku. Poté začne tam, kde skončil, aby získal název dalšího souboru, a spustí další smyčku atd., dokud nedosáhne konce výstupu.
Takže to efektivně dělá toto:
for file in "zquery" "-" "abc" ...
Chcete-li mu sdělit, aby vstup rozdělil pouze na nové řádky, musíte to udělat
IFS=$'n'
před vaším for ... find
příkaz.
Tím nastavíte IFS
na jeden nový řádek, takže se rozděluje pouze na nové řádky a ne také na mezery a tabulátory.
Pokud používáte sh
nebo dash
místo ksh93
, bash
nebo zsh
, musíte napsat IFS=$'n'
místo toho takto:
IFS='
'
To pravděpodobně stačí k tomu, aby váš skript fungoval, ale pokud máte zájem správně zpracovat některé další rohové případy, čtěte dále…
2. Rozbalování $file
bez zástupných znaků
Uvnitř smyčky, kde dělá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.
Může však také obsahovat zástupné znaky, například *
nebo ?
, což by vedlo k nepředvídatelnému chování. (Děkuji Gillesovi za upozornění.)
Chcete-li shellu říci, aby nerozšiřoval zástupné znaky, vložte proměnnou do dvojitých uvozovek, např.
diff "$file" "/some/other/path/$file"
Stejný problém by nás také mohl kousnout
for file in `find . -name "*.csv"`
Pokud jste například měli tyto tři soubory
file1.csv
file2.csv
*.csv
(velmi nepravděpodobné, ale stále možné)
Související:Pokud změním oprávnění u souboru tar, bude to platit i pro soubory v něm?Bylo by to, jako byste běželi
for file in file1.csv file2.csv *.csv
který se rozšíří na
for file in file1.csv file2.csv *.csv file1.csv file2.csv
způsobující file1.csv
a file2.csv
zpracovat 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 řádek na slova podle IFS
a uloží je pod názvy proměnných, které určíte.
Zde mu říkáme, aby nerozděloval řádek na slova a uložil řádek do $file
.
Všimněte si také, že read line
se změnil na read line </dev/tty
.
Je to proto, že uvnitř smyčky přichází standardní vstup z find
prostřednictvím potrubí.
Pokud jsme právě 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é.
Co když pak název souboru obsahuje nové řádky?
Můžeme to vyřešit změnou -print
na -print0
a pomocí read -d ''
na konci potrubí:
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
To umožňuje find
vložte nulový bajt na konec každého názvu souboru. Nulové bajty jsou jediné znaky, které nejsou v názvech souborů povoleny, takže by to mělo zpracovat všechny možné názvy souborů, bez ohledu na to, jak divné jsou.
K získání názvu souboru na druhé straně použijeme IFS= read -r -d ''
.
Kde jsme použili read
výše jsme použili výchozí oddělovač řádků nového řádku, ale nyní find
používá jako oddělovač řádku null. V bash
, nemůžete předat znak NUL v argumentu příkazu (ani vestavěným), ale bash
rozumí -d ''
ve významu odděleno NUL . Použijeme tedy -d ''
aby read
použijte stejný oddělovač řádků jako find
. Všimněte si, že -d $'