Pourquoi la boucle sur la sortie de find est-elle une mauvaise pratique?

170

Cette question est inspirée par

Pourquoi utiliser une boucle shell pour traiter du texte est-il considéré comme une mauvaise pratique?

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 findest-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?

don_crissti
la source
12
Je pense que cela ressemble à "Ne jamais analyser la sortie!" - vous pouvez certainement faire l'un ou l'autre, mais ils sont plus rapides que la qualité de la production. Ou, plus généralement, ne jamais être dogmatique.
Bruce Ediger
Cela devrait être transformé en une réponse canonique
Zaid
6
Parce que le point de découverte est de boucler sur ce qu’il trouve.
OrangeDog
2
Un point accessoire - vous voudrez peut-être envoyer la sortie dans un fichier, puis la traiter plus tard dans le script. De cette façon, la liste de fichiers est disponible pour révision si vous devez déboguer le script.
user117529

Réponses:

87

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 {} +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:

  1. 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.

  2. 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.

  3. 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) )

  4. 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é.

Stéphane Chazelas
la source
Si zsh et bash sont disponibles, il vaut peut- être mieux utiliser des constructions globbing et shell plutôt que d'essayer de contourner le findcomportement en toute sécurité. Globbing est sûr par défaut alors que find est dangereux par défaut.
Kevin
@ Kevin, voir éditer.
Stéphane Chazelas
182

Pourquoi findla sortie de boucle en boucle est-elle une mauvaise pratique?

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 findet 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 findinclut un -print0primaire qui utilisera un octet NULL pour délimiter les noms de fichiers qu’elle imprime, GNU find peut être utilisé en toute sécurité avec GNU xargset son -0drapeau (et son -rdrapeau) pour gérer la sortie de find:

find ... -print0 | xargs -r0 ...

Cependant, il n'y a pas de bonne raison d'utiliser ce formulaire, car:

  1. Cela ajoute une dépendance à GNU findutils qui n’a pas besoin d’être là, et
  2. findest conçu pour pouvoir exécuter des commandes sur les fichiers trouvés.

De plus, GNU xargsrequiert -0et -r, alors que FreeBSD xargsne nécessite que -0(et n’a pas d’ -roption), et certains xargsne le supportent pas -0du tout. Il est donc préférable de s'en tenir aux fonctionnalités POSIX de find(voir section suivante) et de sauter xargs.

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:

findL'activité de l'entreprise consiste à évaluer des expressions et non à localiser des fichiers. Oui, findcertainement localise les fichiers; mais c'est vraiment juste un effet secondaire.

--Unix Power Tools


POSIX utilisations spécifiées de find

Quelle est la manière appropriée d'exécuter une ou plusieurs commandes pour chacun des findrésultats?

Pour exécuter une seule commande pour chaque fichier trouvé, utilisez:

find dirname ... -exec somecommand {} \;

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:

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Pour exécuter une seule commande sur plusieurs fichiers à la fois:

find dirname ... -exec somecommand {} +

find en combinaison avec sh

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 -cconstruction. Vous devriez savoir quelques choses à ce sujet:

  • Ne jamais intégrer {}directement dans le shcode. 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 pourfind :

    Si un nom d' utilitaire ou une chaîne d' argument contient les deux caractères "{}", mais pas uniquement les deux caractères "{}", il est défini par l'implémentation de savoir si find remplace ces deux caractères ou utilise la chaîne sans modification.

    ... Si plus d'un argument contenant les deux caractères "{}" est présent, le comportement n'est pas spécifié.

  • Les arguments suivant la chaîne de commande shell transmise à l' -coption 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 $0valeur "fictive" , telle que find-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 $0signifierait que le premier fichier transmis serait défini sur $0et ne serait donc pas inclus dans "$@".


Pour exécuter une seule commande shell par fichier, utilisez:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

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é:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Notez que cela for f doéquivaut à for f in "$@"; doet gère chacun des paramètres de position l'un après l'autre, autrement dit, il utilise chacun des fichiers trouvés par find, quels que soient les caractères spéciaux de leurs noms.)


Autres exemples d' findutilisation correcte :

(Remarque: n'hésitez pas à étendre cette liste.)

Wildcard
la source
5
Il y a un cas où je ne connais pas d'alternative à findla 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 -u3et 3<sont présents au cas où quelque chose dans la boucle tente de lire stdin.
Gordon Davisson
1
@GordonDavisson, peut - être, mais qu'est-ce que vous devez définir ces variables pour ? Je dirais que quoi que ce soit devrait être traité dans l' find ... -execappel. Ou utilisez simplement un glob shell, s'il gérera votre cas d'utilisation.
Wildcard
1
Je souhaite souvent imprimer un résumé après le traitement des fichiers ("2 convertis, 3 ignorés, les fichiers suivants contiennent des erreurs: ..."), et ces comptes / listes doivent être accumulés dans des variables de shell. En outre, il existe des situations dans lesquelles je souhaite créer un tableau de noms de fichiers afin de pouvoir effectuer des tâches plus complexes que d'effectuer une itération ordonnée (dans ce cas, c'est filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson
3
Votre réponse est correcte. Cependant, je n'aime pas le dogme. Bien que je sache mieux, il existe de nombreux cas d'utilisation (spécialement interactifs) où il est sûr et tout simplement plus facile de taper en boucle sur la findsortie ou même pire ls. 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.
Rudimeier
16
@rudimeier, la discussion sur le dogme contre les meilleures pratiques a déjà été faite à mort . Pas intéressé. Si vous l'utilisez de manière interactive et que cela fonctionne, très bien, tant mieux pour vous - mais je ne vais pas promouvoir cela. Le pourcentage d’auteurs de scripts qui se donnent la peine d’apprendre ce qu’est un code robuste et qui ne le font que lorsqu’ils écrivent des scripts de production, au lieu de se contenter de faire ce qu’ils ont l' habitude de faire de manière interactive, est extrêmement minime. Le traitement consiste à promouvoir les meilleures pratiques tout le temps. Les gens doivent apprendre qu'il existe une bonne façon de faire les choses.
Wildcard
10

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

for file in `find . -type f -name ...`; do smth with ${file}; done

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 findpuis analysée foravant même d’exécuter votre première smth.

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 finden parallèle smthet 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 un smthprocessus par fichier. Si smthne peut fonctionner que sur un seul fichier, il doit en être ainsi.

Dans la mesure du possible, la solution optimale consisterait find -print0 | smthà smthpouvoir traiter les noms de fichiers sur son STDIN. Ensuite, vous n’avez qu’un seul smthprocessus, 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’il smths’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 smthest probable que l’une des meilleures solutions. Comme @ dave_thompson_085 mentionné dans les commentaires, xargsdivise les arguments en plusieurs exécutions smthlorsque les limites système sont atteintes (par défaut, dans la plage de 128 Ko ou quelle que soit la limite imposée par execle système), et dispose d'options permettant d'influer sur les fichiers sont attribués à un appel de smth, ce qui permet de trouver un équilibre entre le nombre de smthprocessus et le retard initial.

EDIT: suppression de la notion de "meilleur" - il est difficile de dire si quelque chose de mieux va apparaître. ;)

AnoE
la source
find ... -exec smth {} +est la solution.
Wildcard
find -print0 | xargs smthne fonctionne pas du tout, mais find -print0 | xargs -0 smth(note -0) ou find | xargs smthsi les noms de fichiers n'ont pas de guillemets ni de barres obliques inverses, l'un d'entre eux smthcomporte 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écutera smthautant 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.
dave_thompson_085
4

Une des raisons est que les espaces blancs jettent une clé, le fichier 'foo bar' est alors évalué comme 'foo' et 'bar'.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Fonctionne bien si -exec est utilisé à la place

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$
steve
la source
Surtout dans le cas de, findpuisqu'il existe une option pour exécuter une commande sur chaque fichier, c'est de loin la meilleure option.
Centimane
1
-exec ... {} \;-exec ... {} +
Voir
1
si vous utilisez for file in "$(find . -type f)" et echo "${file}"puis cela fonctionne même avec des espaces, d'autres caractères spéciaux, je suppose, causent plus de problèmes si
mardi
9
@mazs - non, citer ne fait pas ce que vous pensez. Dans un répertoire contenant plusieurs fichiers, essayez d’ for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";doneimprimer (selon vous) chaque nom de fichier sur une ligne séparée précédée de name:. Ce n'est pas.
don_crissti
2

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:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

Même chose dans le poisson:

for i in **
    echo «$i»
end

Si vous avez besoin des fonctionnalités de find, veillez à ne séparer que sur NUL (comme l' find -print0 | xargs -r0idiome).

Le poisson peut itérer une sortie délimitée par NUL. Donc celui-ci n'est en fait pas mauvais:

find -print0 | while read -z i
    echo «$i»
end

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.

utilisateur2394284
la source
@don_crissti précisément. Cela ne fonctionne généralement pas . J'essayais d'être sarcastique en disant que cela "fonctionne" (avec des citations).
user2394284
Notez que le globbing récursif est apparu au zshdébut des années 90 (bien que vous ayez besoin de **/*là). fishcomme 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.
Stéphane Chazelas
1

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

Jan Kyu Peblik
la source