it-swarm-eu.dev

Opakování v souborech s mezerami v názvech?

Napsal jsem následující skript, abych rozdělil výstupy dvou režisérů do všech stejných souborů jako takových:

#!/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í jiné způsoby, jak toho dosáhnout. Je však zajímavé, že tento skript selže, pokud soubory obsahují mezery. Jak to zvládnu?

Příklad výsledku hledání:

./zQuery - abc - Do Not Prompt for Date.csv
160
Amir Afghani

Krátká odpověď (nejblíže 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ěď (také zpracovává zástupné znaky a řá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ě Gillesova odpověď )

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

Nebo ještě lépe, aby se zabránilo 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
' sh {} +

Dlouhá odpověď

Máte tři problémy:

  1. Ve výchozím nastavení Shell rozděluje výstup příkazu na mezery, karty a nové řádky
  2. Názvy souborů mohou obsahovat zástupné znaky, které by se rozšířily
  3. Co když existuje adresář, jehož jméno končí v *.csv?

1. Rozdělení pouze na nové řádky

Abychom zjistili, na co se má nastavit file, musí Shell vzít výstup find a nějakým způsobem jej interpretovat, jinak by file byl pouze výstupem find.

Shell čte proměnnou IFS, která je ve výchozím nastavení nastavena na <space><tab><newline>.

Poté se podívá na každý znak ve výstupu find. Jakmile uvidí libovolný znak, který je v IFS, myslí si, že označuje konec názvu souboru, takže nastaví file na všechny znaky, které viděl až doposud, a spustí smyčku. Potom začíná tam, kde byl ukončen, aby získal další název souboru, a spustí další smyčku atd., Dokud nedosáhne konce výstupu.

Účinně to dělá:

for file in "zquery" "-" "abc" ...

Chcete-li říci, aby rozdělil vstup pouze na nové řádky, musíte to udělat

IFS=$'\n'

před příkazem for ... find.

To nastaví IFS na jeden nový řádek, takže se rozdělí pouze na nové řádky, nikoli na mezery a karty.

Pokud místo ksh93, sh nebo dash používáte bash nebo zsh, musíte napsat IFS=$'\n' jako to místo toho:

IFS='
'

To je asi dost na to, aby váš skript fungoval, ale pokud máte zájem řešit některé další rohové případy správně, přečtěte si ...

2. Rozšiřuje se $file Bez zástupných znaků

Uvnitř smyčky, kde to děláte

diff $file /some/other/path/$file

shell se snaží expandovat $file (znovu!).

Mohlo by to obsahovat mezery, ale protože jsme již nastavili IFS výše, nebude to problém.

Mohlo by však také obsahovat zástupné znaky jako * Nebo ?, Což by vedlo k nepředvídatelnému chování. (Díky Gilles za to, že jste na to poukázal.)

Chcete-li říct, aby Shell 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é)

Bylo by to, jako kdybyste utekli

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ůsobí, že file1.csv a file2.csv budou zpracovány 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 přečte řádky ze standardního vstupu, rozdělí řádek na slova podle IFS a uloží je do jmen proměnných, které zadáte.

Tady říkáme, aby se nerozdělil řádek na slova a aby se tento řádek neukládal do $file.

Také si všimněte, ž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 potrubím.

Pokud bychom právě udělali read, bylo by to náročné na čá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 v tomto případě to není důležité.

Co když potom název souboru obsahuje nové řádky?

Zvládneme to změnou -print Na -print0 A použitím 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 způsobí, že find vloží nulový bajt na konec každého názvu souboru. Nulové bajty jsou jediné znaky, které nejsou povoleny v názvech souborů, takže by se mělo zpracovávat všechny možné názvy souborů bez ohledu na to, jak divné.

K získání názvu souboru na druhé straně používáme IFS= read -r -d ''.

Tam, kde jsme použili read výše, jsme použili výchozí oddělovač řádků newline, ale nyní find používá jako oddělovač řádků hodnotu null. V bash nemůžete předat znak NUL v argumentu příkazu (i když je zabudovaný), ale bash chápe -d '' Jako význam oddělován NUL . Používáme tedy -d '', Abychom read použili stejný oddělovač řádků jako find. Všimněte si, že -d $'\0', Mimochodem, funguje také dobře, protože bash nepodporující NUL bajty to považuje za prázdný řetězec.

Abychom byli v pořádku, přidáme také -r, Což říká, že nezpracovávají zpětná lomítka v názvech souborů zvlášť. Například bez -r Se \<newline> Odstraní a \n Se převede na n.

Přenosnější způsob psaní, který nevyžaduje bash nebo zsh nebo si pamatuje všechna výše uvedená pravidla týkající se nulových bytů (opět díky Gilles):

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' {} ';'

3. Přeskočení adresářů, jejichž jména končí * .csv

find . -name "*.csv"

bude také odpovídat adresářům, které se nazývají something.csv.

Chcete-li tomu zabránit, přidejte do příkazu find-type f.

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

Jak ukazuje glenn jackman , v obou těchto příkladech jsou příkazy, které se mají provést pro každý soubor, spuštěny v podsloupci, takže pokud změníte jakékoli proměnné uvnitř smyčky, budou zapomenuty.

Pokud potřebujete nastavit proměnné a nechat je stále nastavit na konci smyčky, můžete je přepsat a použít substituci procesu takto:

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 se pokusíte toto zkopírovat a vložit na příkazový řádek, read line Spotřebuje echo "$i files processed", Takže tento příkaz nebude spuštěn.

Chcete-li tomu zabránit, můžete odebrat read line </dev/tty A odeslat výsledek pageru jako less.


[~ # ~] poznámky [~ # ~]

Odstranil jsem středníky (;) Uvnitř smyčky. Můžete je vrátit, pokud chcete, ale nejsou potřeba.

V těchto dnech je $(command) běžnější než `command`. Je to hlavně proto, že je jednodušší napsat $(command1 $(command2)) než `command1 \`command2\``.

read char Ve skutečnosti nečte žádnou postavu. Přečte celou řadu, takže jsem ji změnil na read line.

218
Mikel

Tento skript se nezdaří, pokud jakýkoli název souboru obsahuje mezery nebo Shell globbing znaky \[?*. Příkaz find vydává na jeden řádek jeden název souboru. Poté Shell vyhodnocuje substituci příkazu `find …` Takto:

  1. Spusťte příkaz find, uchopte jeho výstup.
  2. Rozdělte výstup find na samostatná slova. Jakýkoli znak mezery je oddělovač slov.
  3. Pro každé slovo, pokud se jedná o vzor globbing, rozbalte jej do seznamu souborů, které odpovídá.

Předpokládejme například, že v aktuálním adresáři jsou tři soubory nazvané `foo* bar.csv, foo 1.txt A foo 2.txt.

  1. Příkaz find vrátí ./foo* bar.csv.
  2. Shell rozdělí tento řetězec v prostoru a vytvoří dvě slova: ./foo* A bar.csv.
  3. Protože ./foo* Obsahuje metabakteri globbing, je rozšířen do seznamu odpovídajících souborů: ./foo 1.txt A ./foo 2.txt.
  4. Proto je smyčka for prováděna postupně s ./foo 1.txt, ./foo 2.txt A bar.csv.

Většině problémů se můžete v této fázi vyhnout tím, že ztlumíte rozdělení slov a vypnete globbing. Chcete-li tón rozdělit, nastavte proměnnou IFS na jediný znak nového řádku; Tímto způsobem bude výstup find rozdělen na nové řádky a mezery zůstanou. Chcete-li vypnout globlování, spusťte set -f. Tato část kódu pak 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í část vašeho problému, ale doporučuji použít $(…) over `…`. Mají stejný význam, ale verze backquote má divná pravidla citace.)

Níže je další problém: diff $file /some/other/path/$file By měl být

diff "$file" "/some/other/path/$file"

Jinak je hodnota $file Rozdělena na slova a se slovy se zachází jako s globálním vzorem, jako u výše uvedeného příkazu. Pokud si musíte pamatovat na programování prostředí Shell, pamatujte na toto: vždy použijte dvojité uvozovky kolem proměnných rozšíření ($foo) A substituce příkazů ($(bar)), pokud víte, že se chcete rozdělit. (Výše jsme věděli, že chceme rozdělit výstup find na řádky.)

Spolehlivý způsob volání find říká, 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í dvou adresářů, i když musíte všechny „nudné“ soubory výslovně vyloučit.

diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path

Překvapuje mě, že nevidím readarray zmíněné. Je to velmi snadné, když se používá v kombinaci s <<< operátor:

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

Za použití <<<"$expansion"struct také umožňuje rozdělit proměnné obsahující nové řádky do polí, například:

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarray byl v Bash už roky, takže toto by měl být pravděpodobně kanonický způsob, jak toho dosáhnout v Bash.

6
blujay

Smyčka přes všechny soubory ( libovolný zvláštní znak včetně) s zcela bezpečné nalezení (viz odkaz pro dokumentaci):

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
6
l0b0

Afaik find má vše, co potřebujete.

find . -okdir diff {} /some/other/path/{} ";"

find se postará o důvtipné volání programů. -okdir vás vyzve před rozdílem (jste si jisti, že ano/ne).

Nebyl zapojen žádný Shell, žádný kouzelník, žolíci, pi, pa, po.

Jako sidenote: Pokud kombinujete hledání s/while/do/xargs, ve většině případů to děláte špatně. :)

4
user unknown

Jsem překvapen, že zde nikdo nezmínil zřejmé řešení zsh:

for file (**/*.csv(ND.)) {
  do-something-with $file
}

((D) Chcete-li zahrnout i skryté soubory, (N) aby nedošlo k chybě, pokud neexistuje shoda, (.) omezit na normální soubory.)

bash4.3 a výše jej nyní částečně podporuje také:

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done
4

Názvy souborů s mezerami v nich vypadají jako více jmen na příkazovém řádku, pokud nejsou citovány. Pokud je váš soubor s názvem „Hello World.txt“, řádek diff se rozšíří na:

diff Hello World.txt /some/other/path/Hello World.txt

který vypadá jako čtyři názvy souborů. Stačí uvést argumenty kolem argumentů:

diff "$file" "/some/other/path/$file"
2
Ross Smith

Dvojitý citát je váš přítel.

diff "$file" "/some/other/path/$file"

Jinak se obsah proměnné rozdělí na slovo.

1
geekosaur

S bash4 můžete také pomocí vestavěné funkce mapfile nastavit pole obsahující jednotlivé řádky a iterovat v 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
1
kitekat75