Il problema
for f in $(find .)
combina due cose incompatibili.
find
stampa 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 find
e 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 cmd
ha problemi simili (ci sono spazi vuoti, newline, virgolette singole, virgolette doppie e barra rovesciata (e con alcune xarg
implementazioni i byte che non fanno parte di caratteri validi) sono un problema)
Alternative più corrette
L'unico modo per utilizzare un for
loop sull'output di find
sarebbe utilizzare zsh
che supporti IFS=$'\0'
e:
IFS=$'\0'
for f in $(find . -print0)
(sostituirlo -print0
con -exec printf '%s\0' {} +
per find
implementazioni 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 something
può 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 something
che stdin
è la pipe o /dev/null
.
Un motivo che potresti voler usare potrebbe essere quello di utilizzare l' -P
opzione di GNU xargs
per l'elaborazione parallela. Il stdin
problema può anche essere risolto con GNU xargs
con l' -a
opzione 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 something
ciascuna prendendo 20 argomenti di file.
Con zsh
o 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.4
e sopra può anche archiviare i file restituiti da find -print0
in un array con:
readarray -td '' files < <(find . -print0)
L' zsh
equivalente (che ha il vantaggio di preservare lo find
stato di uscita):
files=(${(0)"$(find . -print0)"})
Con zsh
, puoi tradurre la maggior parte delle find
espressioni in una combinazione di globbing ricorsivo con qualificazioni glob. Ad esempio, il looping find . -name '*.txt' -type f -mtime -1
sarebbe:
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).
ksh93
e bash
alla 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, bash
prima 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
/ zsh
trova 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
( find
Almeno GNU ) lo fa aprendo le directory usando openat()
con i O_NOFOLLOW
flag giusti (dove supportati) e mantenendo aperto un descrittore di file per ogni directory, zsh
/ bash
/ ksh
non 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 find
discende correttamente la directory, con -exec cmd {} \;
e ancora di più con -exec cmd {} +
, una volta cmd
eseguita, ad esempio come cmd ./foo/bar
o cmd ./foo/bar ./foo/bar/baz
, al momento in cui si cmd
utilizza ./foo/bar
, gli attributi di bar
potrebbero non soddisfare più i criteri corrispondenti find
, ma anche peggio, ./foo
potrebbero essere stati sostituito da un link simbolico in qualche altro posto (e la finestra della gara è molto più grande con -exec {} +
dove find
attende di avere abbastanza file da chiamare cmd
).
Alcune find
implementazioni hanno un -execdir
predicato (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 -- bar
con alcune implementazioni, quindi il --
), quindi il problema con la ./foo
modifica in un collegamento simbolico viene evitato. Ciò rende l'utilizzo di comandi come rm
più 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 cmd
aumenterà 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 cmd
fa su quel percorso fallirà con un ENAMETOOLONG
errore.
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 find
e 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 74
dovrebbe essere côté.txt
per un'applicazione che interpreta quel nome di file in una locale in cui il set di caratteri è ISO-8859-1 e cєtщ.txt
in 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
/ -path
predicati (e altro, come -iname
o -regex
con alcune implementazioni).
Ciò significa che, ad esempio, con diverse find
implementazioni (inclusa GNU find
).
find . -name '*.txt'
non trova il nostro 63 f4 74 e9 2e 74 78 74
file 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 {} \;
find
troverà correttamente i file il cui nome è composto da 4 caratteri codificati in UTF-8, ma dash
riporterebbe 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é.txt
da precedenti.
Quelli come bash
o in zsh
cui 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 sh
FreeBSD (almeno 11) o mksh -o utf8-mode
che supportano multi-byte, ma solo per UTF-8.
Appunti
1 Per completezza, potremmo menzionare un modo bizzarro di zsh
fare 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 $REPLY
o restituire più file in un $reply
array). Qui eseguiamo l'elaborazione in quella funzione e restituiamo false in modo che il file non sia selezionato.