Le problème
for f in $(find .)
combine deux choses incompatibles.
find
imprime 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 -print
n'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 find
complè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 cmd
des problèmes similaires (des espaces, des nouvelles lignes, des guillemets simples, des guillemets doubles et des barres obliques inverses (et certains xarg
octets 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 for
boucle sur la sortie de find
serait d'utiliser une fonction zsh
qui prend en charge IFS=$'\0'
et:
IFS=$'\0'
for f in $(find . -print0)
(remplacez -print0
par -exec printf '%s\0' {} +
pour les find
implé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 something
peut 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 stdin
est soit le tuyau ou /dev/null
.
Vous voudrez peut-être utiliser l’ -P
option GNU xargs
pour le traitement en parallèle. Le stdin
problème peut également être résolu avec GNU xargs
avec l’ -a
option 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 something
20 arguments de fichier chacun.
Avec zsh
ou bash
, une autre façon de boucler la sortie de find -print0
est 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.4
et ci-dessus peuvent également stocker les fichiers renvoyés par find -print0
dans un tableau avec:
readarray -td '' files < <(find . -print0)
L' zsh
équivalent (qui a l'avantage de préserver find
le statut de sortie):
files=(${(0)"$(find . -print0)"})
Avec zsh
, vous pouvez traduire la plupart des find
expressions en une combinaison de globbing récursif avec des qualificateurs de glob. Par exemple, une boucle find . -name '*.txt' -type f -mtime -1
serait:
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).
ksh93
et bash
finalement 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 bash
qu'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
/ zsh
trouve 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 find
au moins) fait cela en ouvrant les répertoires en utilisant openat()
les bons O_NOFOLLOW
drapeaux (là où ils sont supportés) et en laissant un descripteur de fichier ouvert pour chaque répertoire, zsh
/ bash
/ ksh
ne 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 find
fait descendre le répertoire correctement, avec -exec cmd {} \;
et plus encore avec -exec cmd {} +
, une fois cmd
est exécuté, par exemple comme cmd ./foo/bar
ou cmd ./foo/bar ./foo/bar/baz
, par le temps cmd
utilise ./foo/bar
, les attributs de bar
ne peut plus répondre aux critères assortis par find
, mais pire encore, ./foo
peut-être remplacé par un lien symbolique vers un autre lieu (et la fenêtre de la course est beaucoup plus grande avec -exec {} +
où find
attend d'avoir suffisamment de fichiers à appeler cmd
).
Certaines find
implémentations ont un -execdir
pré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 -- bar
avec 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 rm
safer (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 -- {} +
find
Cela 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é cmd
augmentera 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 cmd
parvient échouera avec une ENAMETOOLONG
erreur.
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' ENAMETOOLONG
erreur est moins susceptible d'être rencontrée.
Octets vs personnages
De plus, find
le 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 74
serait côté.txt
destiné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щ.txt
dans 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!
find
est une de ces applications qui considère les noms de fichiers comme du texte pour ses -name
/ -path
prédicats (et plus, comme -iname
ou -regex
avec certaines implémentations).
Cela signifie que, par exemple, avec plusieurs find
implémentations (y compris GNU find
).
find . -name '*.txt'
ne trouverait pas notre 63 f4 74 e9 2e 74 78 74
fichier 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 {} \;
find
trouvera avec succès les fichiers dont le nom est composé de 4 caractères encodés en UTF-8, mais dash
indiquerait 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é.txt
de plus tôt par exemple.
Ceux comme bash
ou zsh
où 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 sh
de FreeBSD (au moins 11) ou mksh -o utf8-mode
qui supportent plusieurs octets mais uniquement pour UTF-8.
Remarques
1 Par souci d’exhaustivité, nous pourrions mentionner un moyen astucieux de parcourir en zsh
boucle 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)
+cmd
est 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 $REPLY
ou renvoyer plusieurs fichiers dans un $reply
tableau). Ici, nous effectuons le traitement dans cette fonction et renvoyons la valeur false pour que le fichier ne soit pas sélectionné.