Utilizzando un loop come
for i in `find . -name \*.txt`
si interromperà se alcuni nomi di file contengono spazi.
Quale tecnica posso usare per evitare questo problema?
Utilizzando un loop come
for i in `find . -name \*.txt`
si interromperà se alcuni nomi di file contengono spazi.
Quale tecnica posso usare per evitare questo problema?
Risposte:
Idealmente non lo fai affatto, perché analizzare correttamente i nomi dei file in uno script di shell è sempre difficile (correggilo per gli spazi, avrai ancora problemi con altri caratteri incorporati, in particolare newline). Questo è anche elencato come la prima voce nella pagina BashPitfalls.
Detto questo, c'è un modo per fare quasi quello che vuoi:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
Ricorda di citare anche $i
quando lo usi, per evitare che altre cose interpretino gli spazi in un secondo momento. Ricorda anche di $IFS
tornare indietro dopo averlo usato, perché non farlo causerà errori sconcertanti in seguito.
Questo ha un altro avvertimento collegato: ciò che accade all'interno del while
loop può avvenire in una subshell, a seconda della shell esatta che stai usando, quindi le impostazioni variabili potrebbero non persistere. La for
versione loop lo evita, ma al prezzo che, anche se si applica la $IFS
soluzione per evitare problemi con gli spazi, ci si troverà nei guai se find
restituisce troppi file.
Ad un certo punto la correzione corretta per tutto questo diventa farlo in un linguaggio come Perl o Python invece di shell.
Usalo find -print0
e pipe a xargs -0
, oppure scrivi il tuo piccolo programma C e pipe al tuo piccolo programma C. Questo è ciò -print0
per cui -0
sono stati inventati.
Gli script di shell non sono il modo migliore per gestire i nomi di file con spazi: puoi farlo, ma diventa goffo.
È possibile impostare il "separatore di campo interno" ( IFS
) su qualcosa di diverso dallo spazio per la suddivisione dell'argomento loop, ad es
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Ho ripristinato IFS
dopo il suo utilizzo in find, soprattutto perché sembra bello, penso. Non ho riscontrato alcun problema nel impostarlo su Newline, ma penso che sia "più pulito".
Un altro metodo, a seconda di ciò che si desidera fare con l'output da find
, è utilizzare direttamente -exec
con il find
comando oppure utilizzarlo -print0
e reindirizzarlo xargs -0
. Nel primo caso find
si occupa della fuga del nome del file. Nel -print0
caso, find
stampa il suo output con un separatore null, quindi si xargs
divide su questo. Poiché nessun nome di file può contenere quel personaggio (quello che so di), anche questo è sempre sicuro. Questo è utile soprattutto in casi semplici; e di solito non è un ottimo sostituto per un for
ciclo completo .
find -print0
conxargs -0
L'uso find -print0
combinato con xargs -0
è completamente affidabile rispetto ai nomi di file legali ed è uno dei metodi più estensibili disponibili. Ad esempio, supponiamo che tu voglia un elenco di tutti i file PDF nella directory corrente. Potresti scrivere
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Questo troverà tutti i PDF (via -iname '*.pdf'
) nella directory corrente ( .
) e qualsiasi sottodirectory e li passerà come argomento al echo
comando. Poiché abbiamo specificato l' -n 1
opzione, xargs
passerà solo un argomento alla volta a echo
. Se avessimo omesso quell'opzione, xargs
ne avremmo passato il maggior numero possibile echo
. (Puoi echo short input | xargs --show-limits
vedere quanti byte sono ammessi in una riga di comando.)
xargs
esattamente?Possiamo vedere chiaramente l'effetto che xargs
ha sul suo input - e l'effetto di -n
in particolare - usando uno script che fa eco ai suoi argomenti in un modo più preciso di echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Si noti che gestisce perfettamente spazi e newline,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
che sarebbe particolarmente problematico con la seguente soluzione comune:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Appunti
Non sono d'accordo con i bash
basher, perché bash
, insieme al set di strumenti * nix, è abbastanza abile nella gestione dei file (compresi quelli i cui nomi hanno spazi bianchi incorporati).
In realtà, find
ti dà un controllo accurato sulla scelta dei file da elaborare ... Dal lato bash, devi solo capire che devi creare le tue stringhe bash words
; tipicamente usando "virgolette doppie", o qualche altro meccanismo come usare IFS o find's{}
Si noti che nella maggior parte / molte situazioni non è necessario impostare e ripristinare IFS; basta usare IFS localmente come mostrato negli esempi seguenti. Tutti e tre gli spazi bianchi gestiscono bene. Inoltre non è necessaria una struttura di loop "standard", perché find's \;
è effettivamente un loop; basta inserire la logica del loop in una funzione bash (se non si chiama uno strumento standard).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
E, altri due esempi
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'trova also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+ instead
\; `)
find -print0
exargs -0
.