Cette question est inspirée par
Je vois ces constructions
for file in `find . -type f -name ...`; do smth with ${file}; done
et
for dir in $(find . -type d -name ...); do smth with ${dir}; done
être utilisé ici presque quotidiennement, même si certaines personnes prennent le temps de commenter ces publications en expliquant pourquoi ce type de contenu doit être évité ...
Vu le nombre de publications de ce type (et le fait que parfois ces commentaires sont simplement ignorés) Je pensais pouvoir poser une question:
Pourquoi la création de boucles en boucle find
est-elle une mauvaise pratique et quelle est la bonne façon d'exécuter une ou plusieurs commandes pour chaque nom de fichier / chemin renvoyé par find
?
Réponses:
Le problème
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 danszsh
)) danszsh
) (et même l’attaque dans ksh93 ou les dérivés pdksh!).Même si tu le fais:
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 certainsxarg
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 defind
serait d'utiliser une fonctionzsh
qui prend en chargeIFS=$'\0'
et:(remplacez
-print0
par-exec printf '%s\0' {} +
pour lesfind
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
:Ou si
something
peut prendre plus d'un argument:Si vous avez besoin que cette liste de fichiers soit gérée par un shell:
(attention, il peut en démarrer plusieurs
sh
).Sur certains systèmes, vous pouvez utiliser:
si cela a peu d' avantages sur la syntaxe standard et des moyens
something
« sstdin
est soit le tuyau ou/dev/null
.Vous voudrez peut-être utiliser l’
-P
option GNUxargs
pour le traitement en parallèle. Lestdin
problème peut également être résolu avec GNUxargs
avec l’-a
option avec des shells prenant en charge la substitution de processus:par exemple, pour exécuter jusqu'à 4 appels simultanés de
something
20 arguments de fichier chacun.Avec
zsh
oubash
, une autre façon de boucler la sortie defind -print0
est avec: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 parfind -print0
dans un tableau avec:L'
zsh
équivalent (qui a l'avantage de préserverfind
le statut de sortie):Avec
zsh
, vous pouvez traduire la plupart desfind
expressions en une combinaison de globbing récursif avec des qualificateurs de glob. Par exemple, une bouclefind . -name '*.txt' -type f -mtime -1
serait:Ou
(méfiez - vous de la nécessité d'
--
qu'avec**/*
, les chemins de fichiers ne commencent pas avec./
, donc peut commencer-
par exemple).ksh93
etbash
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 égalementbash
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
(GNUfind
au moins) fait cela en ouvrant les répertoires en utilisantopenat()
les bonsO_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 foiscmd
est exécuté, par exemple commecmd ./foo/bar
oucmd ./foo/bar ./foo/bar/baz
, par le tempscmd
utilise./foo/bar
, les attributs debar
ne peut plus répondre aux critères assortis parfind
, 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 à appelercmd
).Certaines
find
implémentations ont un-execdir
prédicat (non encore standard) pour atténuer le second problème.Avec:
find
chdir()
s dans le répertoire parent du fichier avant de l'exécutercmd
. Au lieu d'appelercmd -- ./foo/bar
, il appellecmd -- ./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 querm
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:
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 ycmd
parvient échouera avec uneENAMETOOLONG
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
) quePATH_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
seraitcô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 etcє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 GNUfind
).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, dansfind
trouvera avec succès les fichiers dont le nom est composé de 4 caractères encodés en UTF-8, maisdash
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.dans une locale UTF-8 échouera sur notre ISO-8859-1
côté.txt
de plus tôt par exemple.Ceux comme
bash
ouzsh
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) oumksh -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:+cmd
est un qualificatif global qui appellecmd
(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é.la source
find
comportement en toute sécurité. Globbing est sûr par défaut alors que find est dangereux par défaut.La réponse simple est:
Parce que les noms de fichiers peuvent contenir n’importe quel caractère.
Par conséquent, il n'y a pas de caractère imprimable que vous pouvez utiliser de manière fiable pour délimiter les noms de fichiers.
Les nouvelles lignes sont souvent utilisées (de manière incorrecte) pour délimiter les noms de fichiers, car il est inhabituel d'inclure des caractères de nouvelle ligne dans les noms de fichiers.
Cependant, si vous construisez votre logiciel sur la base d’hypothèses arbitraires, au mieux, vous ne pouvez pas gérer les cas inhabituels et, au pire, vous vous exposez à des exploits malveillants qui cèdent le contrôle de votre système. C'est donc une question de robustesse et de sécurité.
Si vous pouvez écrire un logiciel de deux manières différentes et que l’une d’elles gère correctement les cas extrêmes (entrées inhabituelles), mais que l’autre est plus facile à lire, vous pouvez faire valoir qu’il ya un compromis. (Je ne le ferais pas. Je préfère le code correct.)
Toutefois, si la version correcte et robuste du code est également facile à lire, il n’ya aucune excuse pour écrire du code qui échoue dans les cas extrêmes. C'est le cas
find
et la nécessité d'exécuter une commande sur chaque fichier trouvé.Soyons plus précis: sur un système UNIX ou Linux, les noms de fichiers peuvent contenir n’importe quel caractère, à l’exception de
/
(utilisé comme séparateur de composant de chemin), et ils ne doivent pas contenir d’octet nul.Un octet nul est donc la seule façon correcte de délimiter les noms de fichiers.
Puisque GNU
find
inclut un-print0
primaire qui utilisera un octet NULL pour délimiter les noms de fichiers qu’elle imprime, GNUfind
peut être utilisé en toute sécurité avec GNUxargs
et son-0
drapeau (et son-r
drapeau) pour gérer la sortie defind
:Cependant, il n'y a pas de bonne raison d'utiliser ce formulaire, car:
find
est conçu pour pouvoir exécuter des commandes sur les fichiers trouvés.De plus, GNU
xargs
requiert-0
et-r
, alors que FreeBSDxargs
ne nécessite que-0
(et n’a pas d’-r
option), et certainsxargs
ne le supportent pas-0
du tout. Il est donc préférable de s'en tenir aux fonctionnalités POSIX defind
(voir section suivante) et de sauterxargs
.En ce qui concerne le point 2
find
- la capacité d’exécuter des commandes sur les fichiers qu’il trouve - je pense que Mike Loukides l’a bien dit:POSIX utilisations spécifiées de
find
Pour exécuter une seule commande pour chaque fichier trouvé, utilisez:
Pour exécuter plusieurs commandes en séquence pour chaque fichier trouvé, où la deuxième commande ne doit être exécutée que si la première commande réussit, utilisez:
Pour exécuter une seule commande sur plusieurs fichiers à la fois:
find
en combinaison avecsh
Si vous devez utiliser des fonctionnalités du shell dans la commande, telles que la redirection de la sortie ou la suppression d'une extension du nom de fichier ou quelque chose de similaire, vous pouvez utiliser la
sh -c
construction. Vous devriez savoir quelques choses à ce sujet:Ne jamais intégrer
{}
directement dans lesh
code. Cela permet l'exécution de code arbitraire à partir de noms de fichiers créés de manière malveillante. En outre, POSIX ne spécifie même pas que cela fonctionnera du tout. (Voir le point suivant.)Ne pas utiliser
{}
plusieurs fois, ou l'utiliser dans le cadre d'un argument plus long. Ce n'est pas portable. Par exemple, ne faites pas ceci:find ... -exec cp {} somedir/{}.bak \;
Pour citer les spécifications POSIX pour
find
:Les arguments suivant la chaîne de commande shell transmise à l'
-c
option sont définis sur les paramètres de position du shell, en commençant par$0
. Ne commence pas avec$1
.Pour cette raison, il est bon d’inclure une
$0
valeur "fictive" , telle quefind-sh
, qui sera utilisée pour signaler les erreurs à partir de la coque générée. De plus, cela permet d'utiliser des constructions telles que le"$@"
passage de plusieurs fichiers au shell, alors qu'omettre une valeur pour$0
signifierait que le premier fichier transmis serait défini sur$0
et ne serait donc pas inclus dans"$@"
.Pour exécuter une seule commande shell par fichier, utilisez:
Cependant, le traitement des fichiers dans une boucle de shell donnera de meilleures performances, de sorte que vous ne créiez pas de shell pour chaque fichier trouvé:
(Notez que cela
for f do
équivaut àfor f in "$@"; do
et gère chacun des paramètres de position l'un après l'autre, autrement dit, il utilise chacun des fichiers trouvés parfind
, quels que soient les caractères spéciaux de leurs noms.)Autres exemples d'
find
utilisation correcte :(Remarque: n'hésitez pas à étendre cette liste.)
la source
find
la sortie de l' analyse - où vous devez exécuter des commandes dans le shell actuel (par exemple, parce que vous voulez définir des variables) pour chaque fichier. Dans ce cas,while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)
est le meilleur idiome que je connaisse. Notes:<( )
n'est pas portable - utilisez bash ou zsh. De plus, le-u3
et3<
sont présents au cas où quelque chose dans la boucle tente de lire stdin.find ... -exec
appel. Ou utilisez simplement un glob shell, s'il gérera votre cas d'utilisation.filelist=(); while ... do filelist+=("$file"); done ...
).find
sortie ou même pirels
. Je fais cela quotidiennement sans problèmes. Je connais les options -print0, --ull, -z ou -0 de tout type d’outils. Mais je ne perdrais pas de temps à les utiliser sur mon invite de shell interactive sauf si cela était vraiment nécessaire. Cela pourrait également être noté dans votre réponse.Cette réponse concerne des jeux de résultats très volumineux et concerne principalement les performances, par exemple pour obtenir une liste de fichiers sur un réseau lent. Pour de petites quantités de fichiers (disons quelques 100 voire même 1000 sur un disque local), la plupart de ces problèmes sont sans objet.
Parallélisme et utilisation de la mémoire
Outre les autres réponses données, liées aux problèmes de séparation et autres, il existe un autre problème avec
La partie à l’intérieur des backticks doit être évaluée en premier, avant d’être divisée sur les sauts de ligne. Cela signifie que, si vous obtenez une quantité énorme de fichiers, il peut s’étouffer quelle que soit la taille limite des divers composants; vous risquez de manquer de mémoire s'il n'y a pas de limite; et dans tous les cas, vous devez attendre que toute la liste soit sortie
find
puis analyséefor
avant même d’exécuter votre premièresmth
.La méthode unix préférée consiste à travailler avec des pipes, qui fonctionnent intrinsèquement en parallèle, et qui n'ont pas non plus besoin de tampons arbitrairement énormes en général. Cela signifie: vous préféreriez de beaucoup travailler
find
en parallèlesmth
et ne conserver que le nom de fichier actuel dans la RAM pendant que vous le transmettez àsmth
.Une solution au moins en partie acceptable est la solution susmentionnée
find -exec smth
. Il supprime la nécessité de conserver tous les noms de fichiers en mémoire et fonctionne correctement en parallèle. Malheureusement, il commence également unsmth
processus par fichier. Sismth
ne peut fonctionner que sur un seul fichier, il doit en être ainsi.Dans la mesure du possible, la solution optimale consisterait
find -print0 | smth
àsmth
pouvoir traiter les noms de fichiers sur son STDIN. Ensuite, vous n’avez qu’un seulsmth
processus, quel que soit le nombre de fichiers, et vous ne devez mettre en mémoire tampon qu’une petite quantité d’octets (quelle que soit la mise en mémoire tampon du canal intrinsèque) entre les deux processus. Bien sûr, cela est plutôt irréaliste s’ilsmth
s’agit d’une commande Unix / POSIX standard, mais cela pourrait être une approche si vous l’écrivez vous-même.Si cela n’est pas possible, il
find -print0 | xargs -0 smth
est probable que l’une des meilleures solutions. Comme @ dave_thompson_085 mentionné dans les commentaires,xargs
divise les arguments en plusieurs exécutionssmth
lorsque les limites système sont atteintes (par défaut, dans la plage de 128 Ko ou quelle que soit la limite imposée parexec
le système), et dispose d'options permettant d'influer sur les fichiers sont attribués à un appel desmth
, ce qui permet de trouver un équilibre entre le nombre desmth
processus et le retard initial.EDIT: suppression de la notion de "meilleur" - il est difficile de dire si quelque chose de mieux va apparaître. ;)
la source
find ... -exec smth {} +
est la solution.find -print0 | xargs smth
ne fonctionne pas du tout, maisfind -print0 | xargs -0 smth
(note-0
) oufind | xargs smth
si les noms de fichiers n'ont pas de guillemets ni de barres obliques inverses, l'un d'entre euxsmth
comporte autant de noms de fichiers que possible et ne peut contenir qu'une seule liste d'arguments ; si vous dépassez maxargs, il s'exécuterasmth
autant de fois que nécessaire pour traiter tous les arguments donnés (sans limite). Vous pouvez définir des «morceaux» plus petits (donc un parallélisme un peu plus ancien) avec-L/--max-lines -n/--max-args -s/--max-chars
.Une des raisons est que les espaces blancs jettent une clé, le fichier 'foo bar' est alors évalué comme 'foo' et 'bar'.
Fonctionne bien si -exec est utilisé à la place
la source
find
puisqu'il existe une option pour exécuter une commande sur chaque fichier, c'est de loin la meilleure option.-exec ... {} \;
-exec ... {} +
for file in "$(find . -type f)"
etecho "${file}"
puis cela fonctionne même avec des espaces, d'autres caractères spéciaux, je suppose, causent plus de problèmes sifor file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";done
imprimer (selon vous) chaque nom de fichier sur une ligne séparée précédée dename:
. Ce n'est pas.Parce que la sortie d'une commande est une chaîne unique, mais que votre boucle a besoin d'un tableau de chaînes pour être bouclée. La raison pour laquelle cela "fonctionne" est que les coquillages divisent la chaîne sur les espaces pour vous.
Deuxièmement, à moins que vous n'ayez besoin d'une caractéristique particulière de
find
, gardez à l'esprit que votre shell peut déjà développer un motif glob récursif tout seul et, surtout, qu'il sera étendu à un tableau approprié.Exemple Bash:
Même chose dans le poisson:
Si vous avez besoin des fonctionnalités de
find
, veillez à ne séparer que sur NUL (comme l'find -print0 | xargs -r0
idiome).Le poisson peut itérer une sortie délimitée par NUL. Donc celui-ci n'est en fait pas mauvais:
En dernier recours, dans de nombreux shells (pas Fish bien sûr), le bouclage sur la sortie de la commande transformera le corps de la boucle en sous - shell (ce qui signifie que vous ne pouvez pas définir une variable de quelque manière que ce soit après la fin de la boucle), jamais ce que tu veux.
la source
zsh
début des années 90 (bien que vous ayez besoin de**/*
là).fish
comme précédemment, l'implémentation de la fonctionnalité équivalente de bash suit les liens symboliques lors de la descente de l'arborescence. Voir le résultat de ls *, ls ** et ls *** pour connaître les différences entre les implémentations.Faire une boucle sur la sortie de find n’est pas une mauvaise pratique. La mauvaise pratique (dans toutes les situations) est de supposer que votre entrée est un format particulier au lieu de savoir (tester et confirmer) qu’il s’agit d’un format particulier.
tldr / cbf:
find | parallel stuff
la source