Il problema
for f in $(find .)
combina due cose incompatibili.
findstampa un elenco di percorsi di file delimitati da caratteri di nuova riga. Mentre l'operatore split + glob che viene invocato quando si lascia quello $(find .)non quotato in quel contesto di elenco lo divide sui caratteri di $IFS(per impostazione predefinita include newline, ma anche spazio e tabulazione (e NUL in zsh)) ed esegue il globbing su ogni parola risultante (tranne in zsh) (e persino l'espansione del controvento in derivati ksh93 o pdksh!).
Anche se lo fai:
IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
# but not ksh93)
for f in $(find .) # invoke split+glob
Questo è ancora sbagliato in quanto il carattere di nuova riga è valido come qualsiasi altro in un percorso di file. L'output di non find -printè semplicemente post-processabile in modo affidabile (tranne che usando qualche trucco contorto, come mostrato qui ).
Ciò significa anche che la shell deve archiviare completamente l'output finde quindi dividerlo + glob (che implica la memorizzazione dell'output una seconda volta in memoria) prima di iniziare a eseguire il loop dei file.
Si noti che find . | xargs cmdha problemi simili (ci sono spazi vuoti, newline, virgolette singole, virgolette doppie e barra rovesciata (e con alcune xargimplementazioni i byte che non fanno parte di caratteri validi) sono un problema)
Alternative più corrette
L'unico modo per utilizzare un forloop sull'output di findsarebbe utilizzare zshche supporti IFS=$'\0'e:
IFS=$'\0'
for f in $(find . -print0)
(sostituirlo -print0con -exec printf '%s\0' {} +per findimplementazioni che non supportano il non standard (ma al giorno d'oggi abbastanza comune) -print0).
Qui, il modo corretto e portatile è usare -exec:
find . -exec something with {} \;
O se somethingpuò accettare più di un argomento:
find . -exec something with {} +
Se hai bisogno di un elenco di file che deve essere gestito da una shell:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(attenzione, potrebbe avviarne più di uno sh).
Su alcuni sistemi, è possibile utilizzare:
find . -print0 | xargs -r0 something with
anche se questo ha un piccolo vantaggio rispetto alla sintassi standard e significa somethingche stdinè la pipe o /dev/null.
Un motivo che potresti voler usare potrebbe essere quello di utilizzare l' -Popzione di GNU xargsper l'elaborazione parallela. Il stdinproblema può anche essere risolto con GNU xargscon l' -aopzione con shell che supporta la sostituzione del processo:
xargs -r0n 20 -P 4 -a <(find . -print0) something
ad esempio, per eseguire fino a 4 invocazioni simultanee di somethingciascuna prendendo 20 argomenti di file.
Con zsho bash, un altro modo per eseguire il loop sull'output di find -print0è con:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d '' legge i record delimitati da NUL anziché quelli delimitati da newline.
bash-4.4e sopra può anche archiviare i file restituiti da find -print0in un array con:
readarray -td '' files < <(find . -print0)
L' zshequivalente (che ha il vantaggio di preservare lo findstato di uscita):
files=(${(0)"$(find . -print0)"})
Con zsh, puoi tradurre la maggior parte delle findespressioni in una combinazione di globbing ricorsivo con qualificazioni glob. Ad esempio, il looping find . -name '*.txt' -type f -mtime -1sarebbe:
for file (./**/*.txt(ND.m-1)) cmd $file
O
for file (**/*.txt(ND.m-1)) cmd -- $file
(attenzione alla necessità di --as with **/*, i percorsi dei file non iniziano con ./, quindi potrebbe iniziare con -per esempio).
ksh93e bashalla fine ha aggiunto il supporto per **/(anche se non più fa avanzare forme di globbing ricorsivo), ma ancora non le qualificazioni glob che ne fanno un uso **molto limitato. Inoltre, bashprima di 4.3 segue i collegamenti simbolici quando si discende dall'albero delle directory.
Come per il loop over $(find .), ciò significa anche memorizzare l'intero elenco di file nella memoria 1 . Ciò può essere auspicabile, anche se in alcuni casi quando non si desidera che le azioni sui file influiscano sulla ricerca dei file (come quando si aggiungono altri file che potrebbero finire per essere trovati).
Altre considerazioni su affidabilità / sicurezza
Condizioni di gara
Ora, se stiamo parlando di affidabilità, dobbiamo menzionare le condizioni di gara tra il tempo find/ zshtrova un file e controlla che soddisfi i criteri e il tempo in cui viene utilizzato ( gara TOCTOU ).
Anche quando si discende da un albero di directory, è necessario assicurarsi di non seguire i collegamenti simbolici e di farlo senza la corsa TOCTOU. find( findAlmeno GNU ) lo fa aprendo le directory usando openat()con i O_NOFOLLOWflag giusti (dove supportati) e mantenendo aperto un descrittore di file per ogni directory, zsh/ bash/ kshnon farlo. Quindi, di fronte a un utente malintenzionato che è in grado di sostituire una directory con un collegamento simbolico al momento giusto, si potrebbe finire per discendere la directory sbagliata.
Anche se finddiscende correttamente la directory, con -exec cmd {} \;e ancora di più con -exec cmd {} +, una volta cmdeseguita, ad esempio come cmd ./foo/baro cmd ./foo/bar ./foo/bar/baz, al momento in cui si cmdutilizza ./foo/bar, gli attributi di barpotrebbero non soddisfare più i criteri corrispondenti find, ma anche peggio, ./foopotrebbero essere stati sostituito da un link simbolico in qualche altro posto (e la finestra della gara è molto più grande con -exec {} +dove findattende di avere abbastanza file da chiamare cmd).
Alcune findimplementazioni hanno un -execdirpredicato (non ancora standard) per alleviare il secondo problema.
Con:
find . -execdir cmd -- {} \;
find chdir()s nella directory principale del file prima di eseguirlo cmd. Invece di chiamare cmd -- ./foo/bar, chiama cmd -- ./bar( cmd -- barcon alcune implementazioni, quindi il --), quindi il problema con la ./foomodifica in un collegamento simbolico viene evitato. Ciò rende l'utilizzo di comandi come rmpiù sicuro (potrebbe comunque rimuovere un file diverso, ma non un file in una directory diversa), ma non comandi che possono modificare i file a meno che non siano stati progettati per non seguire i collegamenti simbolici.
-execdir cmd -- {} +a volte funziona anche ma con diverse implementazioni tra cui alcune versioni di GNU find, è equivalente a -execdir cmd -- {} \;.
-execdir ha anche il vantaggio di aggirare alcuni dei problemi associati ad alberi di directory troppo profondi.
Nel:
find . -exec cmd {} \;
la dimensione del percorso assegnato cmdaumenterà con la profondità della directory in cui si trova il file. Se quella dimensione diventa più grande di PATH_MAX(qualcosa come 4k su Linux), allora qualsiasi chiamata di sistema che cmdfa su quel percorso fallirà con un ENAMETOOLONGerrore.
Con -execdir, ./viene passato solo il nome del file (eventualmente con il prefisso ) cmd. I nomi dei file stessi sulla maggior parte dei file system hanno un limite molto più basso ( NAME_MAX) di PATH_MAX, quindi ENAMETOOLONGè meno probabile che si verifichi l' errore.
Byte vs caratteri
Inoltre, spesso trascurato quando si considera la sicurezza intorno finde più in generale con la gestione dei nomi dei file in generale è il fatto che sulla maggior parte dei sistemi simili a Unix, i nomi dei file sono sequenze di byte (qualsiasi valore di byte tranne 0 in un percorso di file e sulla maggior parte dei sistemi ( Per quelli basati su ASCII, per ora ignoreremo quelli rari basati su EBCDIC) 0x2f è il delimitatore del percorso).
Spetta alle applicazioni decidere se vogliono considerare quei byte come testo. E generalmente lo fanno, ma generalmente la traduzione da byte a caratteri avviene in base alle impostazioni locali dell'utente, in base all'ambiente.
Ciò significa che un determinato nome file può avere una rappresentazione testuale diversa a seconda della locale. Ad esempio, la sequenza di byte 63 f4 74 e9 2e 74 78 74dovrebbe essere côté.txtper un'applicazione che interpreta quel nome di file in una locale in cui il set di caratteri è ISO-8859-1 e cєtщ.txtin una locale in cui invece il set di caratteri è IS0-8859-5.
Peggio. In un locale in cui il set di caratteri è UTF-8 (la norma al giorno d'oggi), 63 f4 74 e9 2e 74 78 74 semplicemente non poteva essere mappato sui caratteri!
findè una di queste applicazioni che considera i nomi dei file come testo per i suoi -name/ -pathpredicati (e altro, come -inameo -regexcon alcune implementazioni).
Ciò significa che, ad esempio, con diverse findimplementazioni (inclusa GNU find).
find . -name '*.txt'
non trova il nostro 63 f4 74 e9 2e 74 78 74file sopra quando viene chiamato in una locale UTF-8 poiché *(che corrisponde a 0 o più caratteri , non byte) non può corrispondere a quei non caratteri.
LC_ALL=C find... aggirerebbe il problema poiché la locale C implica un byte per carattere e (generalmente) garantisce che tutti i valori di byte siano associati a un carattere (anche se probabilmente non definiti per alcuni valori di byte).
Ora, quando si tratta di eseguire il loop su quei nomi di file da una shell, quel byte vs carattere può anche diventare un problema. In genere vediamo 4 tipi principali di shell al riguardo:
Quelli che non sono ancora consapevoli del multi-byte come dash. Per loro, un byte è mappato a un personaggio. Ad esempio, in UTF-8, côtésono 4 caratteri, ma 6 byte. In una locale in cui UTF-8 è il set di caratteri, in
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
findtroverà correttamente i file il cui nome è composto da 4 caratteri codificati in UTF-8, ma dashriporterebbe lunghezze comprese tra 4 e 24.
yash: l'opposto. Si tratta solo di personaggi . Tutto l'input che serve viene tradotto internamente in caratteri. Crea la shell più coerente, ma significa anche che non può far fronte a sequenze di byte arbitrarie (quelle che non si traducono in caratteri validi). Anche nella locale C, non può far fronte a valori di byte superiori a 0x7f.
find . -exec yash -c 'echo "$1"' sh {} \;
in una localizzazione UTF-8 non riuscirà ad esempio sul nostro ISO-8859-1 côté.txtda precedenti.
Quelli come basho in zshcui il supporto multi-byte è stato aggiunto progressivamente. Torneranno a considerare byte che non possono essere associati a caratteri come se fossero caratteri. Hanno ancora alcuni bug qua e là, specialmente con set di caratteri multi-byte meno comuni come GBK o BIG5-HKSCS (quelli che sono abbastanza cattivi poiché molti dei loro caratteri multi-byte contengono byte nell'intervallo 0-127 (come i caratteri ASCII) ).
Quelli come shFreeBSD (almeno 11) o mksh -o utf8-modeche supportano multi-byte, ma solo per UTF-8.
Appunti
1 Per completezza, potremmo menzionare un modo bizzarro di zshfare il loop dei file usando il globbing ricorsivo senza memorizzare l'intero elenco in memoria:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmdè un qualificatore glob che chiama cmd(in genere una funzione) con il percorso del file corrente in $REPLY. La funzione restituisce true o false per decidere se il file deve essere selezionato (e può anche modificare $REPLYo restituire più file in un $replyarray). Qui eseguiamo l'elaborazione in quella funzione e restituiamo false in modo che il file non sia selezionato.