Le problème
for f in $(find .)
combine deux choses incompatibles.
findimprime une liste de chemins de fichiers délimités par des caractères de nouvelle ligne. Alors que l’opérateur split + glob qui est appelé lorsque vous laissez ce $(find .)non - indiqué dans cette liste, le contexte le scinde en caractères de $IFS(par défaut, newline, mais aussi espace et tabulation (et NUL dans zsh)) dans zsh) (et même l’attaque dans ksh93 ou les dérivés pdksh!).
Même si tu le fais:
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
C'est toujours faux car le caractère de nouvelle ligne est aussi valide que n'importe quel chemin de fichier. La sortie de find -printn'est tout simplement pas post-processable de manière fiable (sauf en utilisant une astuce compliquée, comme illustré ici ).
Cela signifie également que le shell doit stocker findcomplètement la sortie , puis le scinder + glob (ce qui implique de stocker cette sortie une seconde fois en mémoire) avant de commencer à parcourir en boucle les fichiers.
Notez que find . | xargs cmddes problèmes similaires (des espaces, des nouvelles lignes, des guillemets simples, des guillemets doubles et des barres obliques inverses (et certains xargoctets d'implémentations ne faisant pas partie de caractères valides) posent problème)
Des alternatives plus correctes
La seule façon d'utiliser une forboucle sur la sortie de findserait d'utiliser une fonction zshqui prend en charge IFS=$'\0'et:
IFS=$'\0'
for f in $(find . -print0)
(remplacez -print0par -exec printf '%s\0' {} +pour les findimplémentations qui ne supportent pas le non standard (mais assez courant de nos jours) -print0).
Ici, le moyen correct et portable consiste à utiliser -exec:
find . -exec something with {} \;
Ou si somethingpeut prendre plus d'un argument:
find . -exec something with {} +
Si vous avez besoin que cette liste de fichiers soit gérée par un shell:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(attention, il peut en démarrer plusieurs sh).
Sur certains systèmes, vous pouvez utiliser:
find . -print0 | xargs -r0 something with
si cela a peu d' avantages sur la syntaxe standard et des moyens something« s stdinest soit le tuyau ou /dev/null.
Vous voudrez peut-être utiliser l’ -Poption GNU xargspour le traitement en parallèle. Le stdinproblème peut également être résolu avec GNU xargsavec l’ -aoption avec des shells prenant en charge la substitution de processus:
xargs -r0n 20 -P 4 -a <(find . -print0) something
par exemple, pour exécuter jusqu'à 4 appels simultanés de something20 arguments de fichier chacun.
Avec zshou bash, une autre façon de boucler la sortie de find -print0est avec:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d '' lit les enregistrements délimités par NUL au lieu de ceux délimités par une nouvelle ligne.
bash-4.4et ci-dessus peuvent également stocker les fichiers renvoyés par find -print0dans un tableau avec:
readarray -td '' files < <(find . -print0)
L' zshéquivalent (qui a l'avantage de préserver findle statut de sortie):
files=(${(0)"$(find . -print0)"})
Avec zsh, vous pouvez traduire la plupart des findexpressions en une combinaison de globbing récursif avec des qualificateurs de glob. Par exemple, une boucle find . -name '*.txt' -type f -mtime -1serait:
for file (./**/*.txt(ND.m-1)) cmd $file
Ou
for file (**/*.txt(ND.m-1)) cmd -- $file
(méfiez - vous de la nécessité d' --qu'avec **/*, les chemins de fichiers ne commencent pas avec ./, donc peut commencer -par exemple).
ksh93et bashfinalement ajouté le support pour **/(bien que pas plus de formes avancées de globbing récursif), mais toujours pas les qualificatifs de glob qui rendent l'utilisation de **très limitée là-bas. Notez également bashqu'avant la 4.3, les liens symboliques suivaient lors de la descente de l'arborescence.
Comme pour la boucle $(find .), cela signifie également que vous devez stocker la liste complète des fichiers en mémoire 1 . Cela peut être souhaitable dans certains cas, lorsque vous ne souhaitez pas que vos actions sur les fichiers aient une influence sur la recherche des fichiers (par exemple, lorsque vous ajoutez plus de fichiers susceptibles de se retrouver eux-mêmes).
Autres considérations de fiabilité / sécurité
Conditions de course
Maintenant, si nous parlons de fiabilité, nous devons mentionner les conditions de concurrence entre l'heure find/ zshtrouve un fichier et vérifie qu'il répond aux critères et l'heure à laquelle il est utilisé ( course TOCTOU ).
Même lors de la descente d'une arborescence de répertoires, il faut s'assurer de ne pas suivre les liens symboliques et de le faire sans la race TOCTOU. find(GNU findau moins) fait cela en ouvrant les répertoires en utilisant openat()les bons O_NOFOLLOWdrapeaux (là où ils sont supportés) et en laissant un descripteur de fichier ouvert pour chaque répertoire, zsh/ bash/ kshne le fait pas. Ainsi, face à un attaquant pouvant remplacer un répertoire par un lien symbolique au bon moment, vous risquez de descendre dans le mauvais répertoire.
Même si le findfait descendre le répertoire correctement, avec -exec cmd {} \;et plus encore avec -exec cmd {} +, une fois cmdest exécuté, par exemple comme cmd ./foo/barou cmd ./foo/bar ./foo/bar/baz, par le temps cmdutilise ./foo/bar, les attributs de barne peut plus répondre aux critères assortis par find, mais pire encore, ./foopeut-être remplacé par un lien symbolique vers un autre lieu (et la fenêtre de la course est beaucoup plus grande avec -exec {} +où findattend d'avoir suffisamment de fichiers à appeler cmd).
Certaines findimplémentations ont un -execdirprédicat (non encore standard) pour atténuer le second problème.
Avec:
find . -execdir cmd -- {} \;
find chdir()s dans le répertoire parent du fichier avant de l'exécuter cmd. Au lieu d'appeler cmd -- ./foo/bar, il appelle cmd -- ./bar( cmd -- baravec certaines implémentations, d'où le --), afin d' ./fooéviter le problème d' être changé en un lien symbolique. Cela rend l’utilisation de commandes telles que rmsafer (cela pourrait toujours supprimer un fichier différent, mais pas un fichier situé dans un répertoire différent), mais pas les commandes susceptibles de modifier les fichiers à moins qu’ils ne soient conçus pour ne pas suivre les liens symboliques.
-execdir cmd -- {} +findCela fonctionne parfois aussi, mais avec plusieurs implémentations, dont certaines versions de GNU , cela équivaut à -execdir cmd -- {} \;.
-execdir présente également l’avantage de résoudre certains des problèmes liés à une arborescence de répertoires trop profonde.
Dans:
find . -exec cmd {} \;
la taille du chemin indiqué cmdaugmentera en fonction de la profondeur du répertoire dans lequel se trouve le fichier. Si cette taille est supérieure à PATH_MAX(environ 4 Ko sur Linux), alors tout appel système qui y cmdparvient échouera avec une ENAMETOOLONGerreur.
Avec -execdir, seul le nom du fichier (éventuellement précédé du préfixe ./) est passé à cmd. Les noms de fichiers eux-mêmes sur la plupart des systèmes de fichiers ont une limite beaucoup plus basse ( NAME_MAX) que PATH_MAX, de sorte que l' ENAMETOOLONGerreur est moins susceptible d'être rencontrée.
Octets vs personnages
De plus, findle fait que, sur la plupart des systèmes de type Unix, les noms de fichiers sont des séquences d’octets (toute valeur d’octet sauf 0 dans un chemin de fichier, et généralement sur la plupart des systèmes) Basé sur ASCII, nous allons ignorer les rares basés sur EBCDIC pour le moment) 0x2f est le délimiteur de chemin).
C'est aux applications de décider si elles veulent considérer ces octets sous forme de texte. Et ils le font généralement, mais généralement la traduction d'octets en caractères est effectuée en fonction des paramètres régionaux de l'utilisateur, en fonction de l'environnement.
Cela signifie qu'un nom de fichier donné peut avoir une représentation textuelle différente selon les paramètres régionaux. Par exemple, la séquence d'octets 63 f4 74 e9 2e 74 78 74serait côté.txtdestinée à une application interprétant ce nom de fichier dans une locale où le jeu de caractères est ISO-8859-1 et cєtщ.txtdans une locale où le jeu de caractères est plutôt IS0-8859-5.
Pire. Dans une locale où le jeu de caractères est UTF-8 (la norme de nos jours), 63 f4 74 e9 2e 74 78 74 ne pouvaient tout simplement pas être mappés à des caractères!
findest une de ces applications qui considère les noms de fichiers comme du texte pour ses -name/ -pathprédicats (et plus, comme -inameou -regexavec certaines implémentations).
Cela signifie que, par exemple, avec plusieurs findimplémentations (y compris GNU find).
find . -name '*.txt'
ne trouverait pas notre 63 f4 74 e9 2e 74 78 74fichier ci-dessus lorsqu’il est appelé dans une locale UTF-8 car *(qui correspond à 0 ou plusieurs caractères , pas octets) ne pourrait pas correspondre à ces non-caractères.
LC_ALL=C find... contournerait le problème, car les paramètres régionaux C impliquent un octet par caractère et garantissent (généralement) que toutes les valeurs d’octets sont mappées sur un caractère (même si celles-ci ne sont pas définies pour certaines valeurs d’octets).
Maintenant, quand il s'agit de boucler sur les noms de fichiers d'un shell, cet octet contre caractère peut également devenir un problème. On distingue généralement 4 types de coquilles à cet égard:
Ceux qui ne sont toujours pas conscients de plusieurs octets aiment dash. Pour eux, un octet correspond à un personnage. Par exemple, en UTF-8, côté4 caractères, mais 6 octets. Dans une locale où UTF-8 est le jeu de caractères, dans
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
findtrouvera avec succès les fichiers dont le nom est composé de 4 caractères encodés en UTF-8, mais dashindiquerait des longueurs comprises entre 4 et 24.
yash: L'opposé. Il ne traite que des personnages . Toutes les entrées sont converties en caractères internes. Cela rend le shell le plus cohérent, mais cela signifie également qu'il ne peut pas gérer les séquences d'octets arbitraires (celles qui ne sont pas traduites en caractères valides). Même dans les paramètres régionaux C, il ne peut pas gérer les valeurs d'octet supérieures à 0x7f.
find . -exec yash -c 'echo "$1"' sh {} \;
dans une locale UTF-8 échouera sur notre ISO-8859-1 côté.txtde plus tôt par exemple.
Ceux comme bashou zshoù le support multi-octets a été progressivement ajouté. Ceux-ci retomberont sur des octets qui ne peuvent pas être mappés sur des caractères comme s'il s'agissait de caractères. Ils ont encore quelques bugs ici et là, en particulier avec des jeux de caractères multi-octets moins communs tels que GBK ou BIG5-HKSCS (ceux-ci étant assez méchants, beaucoup de leurs caractères multi-octets contenant des octets dans la plage 0-127 (comme les caractères ASCII) )
Ceux comme le shde FreeBSD (au moins 11) ou mksh -o utf8-modequi supportent plusieurs octets mais uniquement pour UTF-8.
Remarques
1 Par souci d’exhaustivité, nous pourrions mentionner un moyen astucieux de parcourir en zshboucle les fichiers à l’aide de la méthode de recalage récursif sans stocker la liste complète en mémoire:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmdest un qualificatif global qui appelle cmd(généralement une fonction) avec le chemin du fichier actuel dans $REPLY. La fonction renvoie true ou false pour décider si le fichier doit être sélectionné (et peut également modifier $REPLYou renvoyer plusieurs fichiers dans un $replytableau). Ici, nous effectuons le traitement dans cette fonction et renvoyons la valeur false pour que le fichier ne soit pas sélectionné.