(8 risposte)
Chiuso 3 anni fa.
Ho scritto il seguente script per differenziare gli output di due directory con tutti gli stessi file in quanto tali:
#!/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, però, questo script non riesce quando i file contengono spazi. Come posso affrontarlo?
Esempio di output di trova:
./zQuery - abc - Do Not Prompt for Date.csv
Risposta accettata:
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"
Risposta migliore (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
Miglior risposta (basata sulla 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
' exec-sh {} ';'
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
' exec-sh {} +
Risposta lunga
Hai tre problemi:
- Per impostazione predefinita, la shell suddivide l'output di un comando in spazi, tabulazioni e nuove righe
- I nomi dei file potrebbero contenere caratteri jolly che verrebbero espansi
- Cosa succede se esiste una directory il cui nome termina con
*.csv
?
Per capire cosa impostare file
a, la shell deve prendere l'output di find
e interpretarlo in qualche modo, altrimenti file
sarebbe solo l'intero output di find
.
La shell legge IFS
variabile, 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 segni la fine del nome del file, quindi imposta file
a tutti i personaggi che ha visto fino ad ora e 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 for ... find
comando.
Ciò imposta IFS
su una singola nuova riga, quindi si divide solo sulle 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'
così invece:
IFS='
'
Probabilmente è abbastanza per far funzionare il tuo script, ma se sei interessato a gestire correttamente alcuni altri casi d'angolo, continua a leggere...
$file
senza caratteri jolly
All'interno del ciclo in cui lo fai
diff $file /some/other/path/$file
la shell tenta di espandere $file
(di nuovo!).
Potrebbe contenere spazi, ma poiché abbiamo già impostato IFS
sopra, non sarà un problema qui.
Ma potrebbe anche contenere caratteri jolly come *
o ?
, che porterebbe a comportamenti imprevedibili. (Grazie a Gilles per averlo fatto notare.)
Per dire alla shell di non espandere i caratteri jolly, metti la variabile tra virgolette doppie, ad es.
diff "$file" "/some/other/path/$file"
Lo stesso problema potrebbe morderci anche noi
for file in `find . -name "*.csv"`
Ad esempio, se avessi questi tre file
file1.csv
file2.csv
*.csv
(molto improbabile, ma comunque possibile)
Correlati:se cambio le autorizzazioni su un file tar, si applicherà ai file al suo interno?Sarebbe come se avessi corso
for file in file1.csv file2.csv *.csv
che verrà ampliato a
for file in file1.csv file2.csv *.csv file1.csv file2.csv
causando file1.csv
e file2.csv
da elaborare 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 dallo standard input, divide la riga in parole secondo IFS
e li memorizza nei nomi delle variabili specificati.
Qui, gli stiamo dicendo di non dividere la riga in parole e di memorizzare la riga in $file
.
Nota anche che read line
è cambiato in read line </dev/tty
.
Questo perché all'interno del ciclo, l'input standard proviene da find
tramite la pipeline.
Se avessimo appena 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. Nota che ciò causerà un errore se lo script viene eseguito tramite cron, ma presumo che non sia importante in questo caso.
Quindi, cosa succede se il nome di un file contiene nuove righe?
Possiamo gestirlo modificando -print
a -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 find
metti un byte nullo alla fine di ogni nome di file. I byte nulli sono gli unici caratteri non consentiti nei nomi di file, quindi questo dovrebbe gestire tutti i possibili nomi di file, non importa quanto strani.
Per ottenere il nome del file sull'altro lato, utilizziamo IFS= read -r -d ''
.
Dove abbiamo usato read
sopra, abbiamo usato il delimitatore di riga predefinito di newline, ma ora, find
utilizza null come delimitatore di riga. In bash
, non puoi passare un carattere NUL in un argomento a un comando (anche quelli integrati), ma bash
comprende -d ''
nel senso di delimitato NUL . Quindi usiamo -d ''
per far read
usa lo stesso delimitatore di riga di find
. Nota che -d $'