it-swarm.it

Riproduzione ciclica di file con spazi nei nomi?

Ho scritto il seguente script per diffondere gli output di due registi con tutti gli stessi file in essi contenuti:

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

So che ci sono altri modi per raggiungere questo obiettivo. Curiosamente, questo script fallisce quando i file contengono spazi. Come posso gestirlo?

Esempio di output di find:

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

Risposta breve (la più vicina alla tua risposta, ma gestisce gli spazi)

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"

Migliore risposta (gestisce anche caratteri jolly e newline nei nomi dei file)

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

Migliore risposta (basata su risposta di Gilles )

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

O ancora meglio, per evitare di eseguire un sh per file:

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 {} +

Risposta lunga

Hai tre problemi:

  1. Per impostazione predefinita, Shell divide l'output di un comando in spazi, schede e newline
  2. I nomi dei file potrebbero contenere caratteri jolly che verrebbero espansi
  3. Cosa succede se esiste una directory il cui nome termina in *.csv?

1. Dividi solo su newline

Per capire su cosa impostare file, Shell deve prendere l'output di find e interpretarlo in qualche modo, altrimenti file sarebbe solo l'intero output di find.

Shell legge la variabile IFS, che è impostata su <space><tab><newline> Per impostazione predefinita.

Quindi esamina ogni carattere nell'output di find. Non appena vede un carattere che si trova in IFS, pensa che segna la fine del nome del file, quindi imposta file su tutti i caratteri che ha visto fino ad ora ed esegue il ciclo. Quindi inizia da dove era stato interrotto per ottenere il nome del file successivo ed esegue il ciclo successivo, ecc., Fino a raggiungere la fine dell'output.

Quindi sta effettivamente facendo questo:

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

Per dirgli di dividere l'input solo su newline, devi farlo

IFS=$'\n'

prima del tuo comando for ... find.

Ciò imposta IFS su una singola nuova riga, quindi si divide solo su nuove righe e non anche su spazi e tabulazioni.

Se stai usando sh o dash invece di ksh93, bash o zsh, devi scrivere IFS=$'\n' come questo invece:

IFS='
'

Questo è probabilmente sufficiente per far funzionare il tuo script, ma se sei interessato a gestire correttamente altri casi angolari, continua a leggere ...

2. Espansione $file Senza caratteri jolly

All'interno del loop dove lo fai

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

shell tenta di espandere $file (di nuovo!).

Potrebbe contenere spazi, ma poiché abbiamo già impostato IFS sopra, non sarà un problema qui.

Ma potrebbe contenere anche caratteri jolly come * O ?, Il che porterebbe a comportamenti imprevedibili. (Grazie a Gilles per averlo segnalato.)

Per dire a Shell di non espandere i caratteri jolly, inserisci la variabile tra virgolette doppie, ad es.

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

Lo stesso problema potrebbe anche morderci

for file in `find . -name "*.csv"`

Ad esempio, se avessi questi tre file

file1.csv
file2.csv
*.csv

(molto improbabile, ma ancora possibile)

Sarebbe come se tu fossi scappato

for file in file1.csv file2.csv *.csv

che verrà espanso a

for file in file1.csv file2.csv *.csv file1.csv file2.csv

causando l'elaborazione file1.csv e file2.csv due volte.

Invece, dobbiamo fare

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 legge le righe dall'input standard, suddivide la riga in parole secondo IFS e le memorizza nei nomi delle variabili specificati.

Qui, gli stiamo dicendo di non dividere la linea in parole e di memorizzare la linea in $file.

Si noti inoltre che read line È stato modificato in read line </dev/tty.

Questo perché all'interno del loop, l'input standard proviene da find tramite la pipeline.

Se facessimo semplicemente read, consumerebbe parte o tutto il nome di un file e alcuni file verrebbero saltati.

/dev/tty È il terminale da cui l'utente esegue lo script. Si noti che ciò causerà un errore se lo script viene eseguito tramite cron, ma presumo che questo non sia importante in questo caso.

Quindi, cosa succede se un nome file contiene nuove righe?

Possiamo gestirlo cambiando -print In -print0 E usando read -d '' Alla fine di una 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

Questo fa sì che find inserisca un byte null alla fine di ogni nome di file. I byte null sono gli unici caratteri non consentiti nei nomi dei file, quindi questo dovrebbe gestire tutti i possibili nomi dei file, non importa quanto siano strani.

Per ottenere il nome del file dall'altra parte, usiamo IFS= read -r -d ''.

Dove abbiamo usato read sopra, abbiamo usato il delimitatore di linea predefinito di newline, ma ora find sta usando null come delimitatore di linea. In bash, non puoi passare un carattere NUL in un argomento a un comando (anche quelli incorporati), ma bash capisce -d '' Come significato NUL delimitato . Quindi usiamo -d '' Per fare in modo che read usi lo stesso delimitatore di riga di find. Nota che -d $'\0', Per inciso, funziona anche perché bash che non supporta byte NUL lo considera come una stringa vuota.

Per essere corretti, aggiungiamo anche -r, Che dice di non gestire le barre rovesciate nei nomi dei file in particolare. Ad esempio, senza -r, \<newline> Vengono rimossi e \n Viene convertito in n.

Un modo più portatile di scrivere questo che non richiede bash o zsh o ricordare tutte le regole di cui sopra sui byte null (di nuovo, grazie a Gilles):

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

3. Saltare le directory i cui nomi finiscono in * .csv

find . -name "*.csv"

corrisponderà anche alle directory chiamate something.csv.

Per evitarlo, aggiungi -type f Al comando find.

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

Come glenn jackman sottolinea, in entrambi questi esempi, i comandi da eseguire per ciascun file vengono eseguiti in una subshell, quindi se si modificano le variabili all'interno del ciclo, verranno dimenticate.

Se è necessario impostare le variabili e impostarle ancora alla fine del ciclo, è possibile riscriverle per utilizzare la sostituzione del processo in questo modo:

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"

Nota che se provi a copiarlo e incollarlo dalla riga di comando, read line Consumerà echo "$i files processed", Quindi quel comando non verrà eseguito.

Per evitare ciò, è possibile rimuovere read line </dev/tty E inviare il risultato a un cercapersone come less.


[~ ~] # Note [~ ~ #]

Ho rimosso i punti e virgola (;) All'interno del ciclo. Puoi rimetterli se vuoi, ma non sono necessari.

In questi giorni, $(command) è più comune di `command`. Ciò è dovuto principalmente al fatto che è più semplice scrivere $(command1 $(command2)) rispetto a `command1 \`command2\``.

read char Non legge davvero un personaggio. Legge un'intera riga, quindi l'ho cambiata in read line.

218
Mikel

Questo script ha esito negativo se un nome file contiene spazi o caratteri globbing Shell \[?*. Il comando find genera un nome file per riga. Quindi la sostituzione del comando `find …` Viene valutata dalla Shell come segue:

  1. Esegui il comando find, prendine l'output.
  2. Dividi l'output find in parole separate. Qualsiasi carattere di spazio bianco è un separatore di parole.
  3. Per ogni parola, se si tratta di un modello globbing, espanderlo all'elenco dei file corrispondenti.

Ad esempio, supponiamo che ci siano tre file nella directory corrente, chiamati `foo* bar.csv, foo 1.txt E foo 2.txt.

  1. Il comando find restituisce ./foo* bar.csv.
  2. Shell divide questa stringa nello spazio, producendo due parole: ./foo* E bar.csv.
  3. Poiché ./foo* Contiene un metacarattero sconvolgente, viene espanso nell'elenco dei file corrispondenti: ./foo 1.txt E ./foo 2.txt.
  4. Pertanto il ciclo for viene eseguito in successione con ./foo 1.txt, ./foo 2.txt E bar.csv.

È possibile evitare la maggior parte dei problemi in questa fase attenuando la divisione delle parole e disattivando il globbing. Per attenuare la suddivisione in Word, imposta la variabile IFS su un singolo carattere di nuova riga; in questo modo l'output di find verrà diviso solo a newline e gli spazi rimarranno. Per disattivare il globbing, esegui set -f. Quindi questa parte del codice funzionerà finché nessun nome di file contiene un carattere di nuova riga.

IFS='
'
set -f
for file in $(find . -name "*.csv"); do …

(Questo non fa parte del tuo problema, ma ti consiglio di usare $(…) su `…`. Hanno lo stesso significato, ma la versione di backquote ha strane regole di quotazione.)

C'è un altro problema qui sotto: diff $file /some/other/path/$file Dovrebbe essere

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

Altrimenti, il valore di $file Viene diviso in parole e le parole vengono trattate come modelli glob, come con il comando sostitutivo sopra. Se devi ricordare una cosa sulla programmazione di Shell, ricorda questo: sa sempre le virgolette doppie intorno alle espansioni variabili ($foo) E alle sostituzioni di comandi ($(bar)), a meno che tu non so che vuoi dividere. (Sopra, sapevamo di voler dividere l'output find in righe.)

Un modo affidabile per chiamare find è dire di eseguire un comando per ogni file che trova:

find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'

In questo caso, un altro approccio è quello di confrontare le due directory, anche se è necessario escludere esplicitamente tutti i file "noiosi".

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

Sono sorpreso di non vedere readarray menzionato. Lo rende molto semplice se usato in combinazione con <<< operatore:

$ touch oneword "two words"

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

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

Usando il <<<"$expansion" construct ti consente anche di dividere le variabili contenenti newline in array, come:

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

readarray è in Bash da anni ormai, quindi questo dovrebbe probabilmente essere il modo canonico per farlo in Bash.

6
blujay

Scorri tutti i file ( qualsiasi carattere speciale incluso) con trova completamente sicuro (vedi il link per la documentazione):

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 ha tutto ciò di cui hai bisogno.

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

find si occupa di chiamare i programmi in modo sicuro. -okdir ti chiederà prima del diff (sei sicuro di sì/no).

Nessuna Shell coinvolta, nessun ostacolo, jolly, pi, pa, po.

Come sidenote: se combini find con for/while/do/xargs, nella maggior parte dei casi stai sbagliando. :)

4
user unknown

Sono sorpreso che nessuno abbia ancora menzionato l'ovvia soluzione zsh:

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

((D) per includere anche file nascosti, (N) per evitare l'errore se non c'è corrispondenza, (.) per limitare a normale file.)

bash4.3 e versioni successive ora lo supportano anche parzialmente:

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

I nomi dei file con spazi al loro interno sembrano più nomi sulla riga di comando se non sono quotati. Se il tuo file è denominato "Hello World.txt", la riga diff si espande in:

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

che assomiglia a quattro nomi di file. Metti solo virgolette intorno agli argomenti:

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

La doppia citazione è tua amica.

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

Altrimenti il ​​contenuto della variabile viene diviso in Word.

1
geekosaur

Con bash4, puoi anche usare la funzione mapfile incorporata per impostare un array contenente ciascuna riga e iterare su questo array.

$ 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