pour vs trouver dans Bash

28

Lorsque vous parcourez des fichiers, il y a deux façons:

  1. utilisez un for-loop:

    for f in *; do
        echo "$f"
    done
    
  2. utiliser find:

    find * -prune | while read f; do 
        echo "$f"
    done
    

En supposant que ces deux boucles trouveront la même liste de fichiers, quelles sont les différences entre ces deux options en termes de performances et de manipulation?

rubo77
la source
1
Pourquoi? findn'ouvre pas les fichiers qu'il trouve. La seule chose que je peux voir vous mordre ici en ce qui concerne un grand nombre de fichiers est ARG_MAX .
kojiro
1
Consultez les réponses et les commentaires qui vous indiquent que read fles noms de fichiers seront modifiés lors de leur lecture (par exemple, les noms avec des blancs en tête). find * -pruneSemble également être une manière très compliquée de dire simplement ls -1oui?
Ian D. Allen
4
Ne présumez pas que les deux boucles trouveront le même ensemble de fichiers; dans la plupart des cas, ils ne le feront pas. En outre, cela devrait être find ., non find *.
alexis
1
@terdon Oui, l'analyse ls -lest une mauvaise idée. Mais l'analyse ls -1(ce n'est 1pas un l) n'est pas pire que l'analyse find * -prune. Les deux échouent sur les fichiers avec des retours à la ligne dans les noms.
Ian D. Allen
5
Je soupçonne que nous avons chacun passé plus de temps à lire cette question et ces réponses que la différence totale de performances sur la durée de vie du script en question.
mpez0

Réponses:

9

1.

Le premier:

for f in *; do
  echo "$f"
done

échoue pour les fichiers appelés -n, -eet des variantes comme -neneet avec quelques déploiements bash, avec les noms de fichiers contenant des barres obliques inverses.

La deuxième:

find * -prune | while read f; do 
  echo "$f"
done

échoue pour les cas encore plus (fichiers appelés !, -H, -name, (, noms de fichiers qui commencent ou se terminent par des blancs ou contiennent des caractères ... saut de ligne)

C'est le shell qui se développe *, findne fait qu'imprimer les fichiers qu'il reçoit comme arguments. Vous pourriez aussi bien avoir utilisé à la printf '%s\n'place ce qui, comme il printfest intégré, éviterait également l' erreur potentielle de trop d'arguments .

2.

L'expansion de *est triée, vous pouvez la rendre un peu plus rapide si vous n'avez pas besoin du tri. Dans zsh:

for f (*(oN)) printf '%s\n' $f

ou simplement:

printf '%s\n' *(oN)

bashn'a pas d'équivalent pour autant que je sache, vous devez donc y recourir find.

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(ci-dessus en utilisant une -print0extension non standard GNU / BSD ).

Cela implique toujours de générer une commande find et d'utiliser une while readboucle lente , donc ce sera probablement plus lent que d'utiliser la forboucle à moins que la liste des fichiers ne soit énorme.

4.

De plus, contrairement à l'expansion du shell, les caractères génériques findeffectueront un lstatappel système sur chaque fichier, il est donc peu probable que le non-tri compense cela.

Avec GNU / BSD find, cela peut être évité en utilisant leur -maxdepthextension qui déclenchera une optimisation en sauvant lstat:

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Parce que findcommence la sortie des noms de fichiers dès qu'il les trouve (à l'exception de la mise en mémoire tampon de sortie stdio), où cela peut être plus rapide, c'est si ce que vous faites dans la boucle prend du temps et la liste des noms de fichiers est plus qu'un tampon stdio (4 / 8 kB). Dans ce cas, le traitement dans la boucle commencera avant d' findavoir fini de trouver tous les fichiers. Sur les systèmes GNU et FreeBSD, vous pouvez utiliser stdbufpour que cela se produise plus tôt (désactivation de la mise en mémoire tampon stdio).

5.

La manière POSIX / standard / portable d'exécuter des commandes pour chaque fichier avec findest d'utiliser le -execprédicat:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

Dans le cas de echocela, c'est moins efficace que de faire une boucle dans le shell car le shell aura une version intégrée de echowhile finddevra générer un nouveau processus et l'exécuter /bin/echopour chaque fichier.

Si vous devez exécuter plusieurs commandes, vous pouvez faire:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Mais attention, elle cmd2n'est exécutée qu'en cas de cmd1succès.

6.

Une manière canonique d'exécuter des commandes complexes pour chaque fichier est d'appeler un shell avec -exec ... {} +:

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

Cette fois-ci, nous sommes redevenus efficaces echocar nous shutilisons celui intégré à et la -exec +version apparaît le moins shpossible.

7.

Dans mes tests sur un répertoire avec 200.000 fichiers avec des noms courts sur ext4, zshcelui (paragraphe 2.) est de loin le plus rapide, suivi de la première for i in *boucle simple (bien que comme d'habitude, bashest beaucoup plus lente que les autres shells pour cela).

Stéphane Chazelas
la source
que fait la !commande find?
rubo77
@ rubo77, !c'est pour la négation. ! -name . -prune more...fera -prune(et more...depuis -pruneretourne toujours vrai) pour chaque fichier mais .. Donc, cela se fera more...sur tous les fichiers de ., mais exclura .et ne descendra pas dans les sous-répertoires de .. C'est donc l'équivalent standard des GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas
18

J'ai essayé ceci sur un répertoire avec 2259 entrées et j'ai utilisé la timecommande.

La sortie de time for f in *; do echo "$f"; done(moins les fichiers!) Est:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

La sortie de time find * -prune | while read f; do echo "$f"; done(moins les fichiers!) Est:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

J'ai exécuté chaque commande plusieurs fois, afin d'éliminer les ratés du cache. Cela suggère de le conserver bash(pour i dans ...) est plus rapide que d'utiliser findet de canaliser la sortie (vers bash)

Juste pour être complet, j'ai laissé tomber le tuyau find, car dans votre exemple, il est entièrement redondant. La sortie de juste find * -pruneest:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Aussi, time echo *(la sortie n'est pas séparée par des sauts de ligne, hélas):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

À ce stade, je pense que la raison echo *est plus rapide, car il ne génère pas autant de nouvelles lignes, de sorte que la sortie ne défile pas autant. Testons ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

rendements:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

tandis que les time find * -prune > /dev/nullrendements:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

et time for f in *; do echo "$f"; done > /dev/nulldonne:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

et enfin: time echo * > /dev/nulldonne:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Une partie de la variation peut être expliquée par des facteurs aléatoires, mais cela semble clair:

  • la sortie est lente
  • la tuyauterie coûte un peu
  • for f in *; do ...est plus lent que find * -prune, à lui seul, mais pour les constructions ci-dessus impliquant des tuyaux, est plus rapide.

En outre, en passant, les deux approches semblent gérer très bien les noms avec des espaces.

MODIFIER:

Timings pour find . -maxdepth 1 > /dev/nullvs find * -prune > /dev/null:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Donc, conclusion supplémentaire:

  • find * -pruneest plus lent que find . -maxdepth 1- dans le premier, le shell traite un glob, puis construit une (grande) ligne de commande pour find. NB: find . -prunerevient juste ..

Plus de tests time find . -maxdepth 1 -exec echo {} \; >/dev/null::

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Conclusion:

  • moyen le plus lent de le faire jusqu'à présent. Comme cela a été souligné dans les commentaires pour la réponse où cette approche a été suggérée, chaque argument engendre une coquille.
Phil
la source
Quel tuyau est redondant? pouvez-vous montrer la ligne que vous avez utilisée sans tuyau?
rubo77
2
@ rubo77 find * -prune | while read f; do echo "$f"; donea le tuyau redondant - tout ce que fait le tuyau est de produire exactement ce qui findsort de lui-même. Sans pipe, ce serait simplement. find * -prune La pipe n'est redondante que parce que la chose de l'autre côté de la pipe copie simplement stdin vers stdout (pour la plupart). C'est un no-op cher. Si vous voulez faire des trucs avec la sortie de find, autre que simplement le cracher à nouveau, c'est différent.
Phil
Peut-être que la principale chronologie est le *. Comme l'a déclaré BitsOfNix : Je suggère toujours fortement de ne pas utiliser *et .pour à la findplace.
rubo77
@ rubo77 semble ainsi. Je suppose que j'ai oublié cela. J'ai ajouté des résultats pour mon système. Je suppose que find . -prunec'est plus rapide car findlira une entrée de répertoire textuellement, tandis que le shell fera de même, potentiellement en correspondance avec le glob (pourrait être optimisé pour *), puis en construisant la grande ligne de commande pour find.
Phil
1
find . -pruneimprime uniquement .sur mon système. Cela ne fonctionne presque pas du tout. Ce n'est pas du tout le même que find * -prunecelui qui montre tous les noms du répertoire courant. Un nom nu read fmodifiera les noms de fichiers avec des espaces de début.
Ian D. Allen
10

J'irais certainement avec find bien que je changerais votre trouvaille en ceci:

find . -maxdepth 1 -exec echo {} \;

En termes de performances, findc'est beaucoup plus rapide en fonction de vos besoins bien sûr. Ce que vous avez actuellement avec forlui n'affichera que les fichiers / répertoires dans le répertoire courant mais pas le contenu des répertoires. Si vous utilisez find, il affichera également le contenu des sous-répertoires.

Je dis trouver est mieux depuis votre forla *devra être élargie d' abord et je crains que si vous avez un répertoire avec une énorme quantité de fichiers , il peut donner l'erreur liste d'arguments trop long . De même pourfind *

Par exemple, dans l'un des systèmes que j'utilise actuellement, il y a quelques répertoires avec plus de 2 millions de fichiers (<100k chacun):

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
la source
J'ai ajouté -prunepour rendre les deux exemples plus semblables. et je préfère la pipe avec while alors il est plus facile d'appliquer plus de commandes dans la boucle
rubo77
changer la limite dure n'est pas une solution de contournement appropriée de mon POV. Surtout quand on parle de plus de 2 millions de fichiers. Sans dérivation de la question, pour les cas simples, un répertoire à un niveau est plus rapide, mais si vous changez la structure de votre fichier / répertoire, il sera plus difficile de migrer. Alors qu'avec find et son énorme quantité d'options, vous pouvez être mieux préparé. Je suggère toujours fortement de ne pas utiliser * et. pour trouver à la place. Ce serait plus portable que * où vous pourriez ne pas être en mesure de contrôler le hardlimit ...
BitsOfNix
4
Cela engendrera un processus d'écho par fichier (tandis que dans le shell for loop, c'est le module intégré d'écho qui sera utilisé sans bifurquer un processus supplémentaire), et descendra dans les répertoires, ce sera donc beaucoup plus lent . Notez également qu'il comprendra des fichiers dot.
Stéphane Chazelas
Vous avez raison, j'ai ajouté le maxdepth 1 pour qu'il ne reste qu'au niveau actuel.
BitsOfNix
7
find * -prune | while read f; do 
    echo "$f"
done

est une utilisation inutile de find- Ce que vous dites est efficace "pour chaque fichier du répertoire ( *), ne trouvez aucun fichier. De plus, ce n'est pas sûr pour plusieurs raisons:

  • Les barres obliques inverses dans les chemins sont traitées spécialement sans la -rpossibilité de read. Ce n'est pas un problème avec la forboucle.
  • Les sauts de ligne dans les chemins briseraient toute fonctionnalité non triviale à l'intérieur de la boucle. Ce n'est pas un problème avec la forboucle.

La gestion de n'importe quel nom de fichier findest difficile , vous devez donc utiliser l' foroption de boucle chaque fois que possible pour cette seule raison. De plus, l'exécution d'un programme externe comme findsera généralement plus lente que l'exécution d'une commande de boucle interne comme for.

l0b0
la source
@ I0b0 Qu'en est-il de find -path './*' -prune or find -path './−^^. ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
1
Ni findles -print0ni ni xargsne -0sont compatibles avec POSIX, et vous ne pouvez pas mettre de commandes arbitraires sh -c ' ... '(les guillemets simples ne peuvent pas être échappés entre guillemets simples), donc ce n'est pas si simple.
l0b0
4

Mais nous sommes des ventouses pour les questions de performances! Cette demande d'expérimentation fait au moins deux hypothèses qui la rendent très peu valable.

A. Supposons qu'ils trouvent les mêmes fichiers…

Eh bien, ils vont trouver les mêmes fichiers dans un premier temps , parce qu'ils sont tous les deux itérer sur la même glob, à savoir *. Mais find * -prune | while read fsouffre de plusieurs défauts qui rendent tout à fait possible qu'il ne trouvera pas tous les fichiers que vous attendez:

  1. La recherche POSIX n'est pas garantie d'accepter plus d'un argument de chemin. La plupart des findimplémentations le font, mais vous ne devriez pas vous fier à cela.
  2. find *peut se casser lorsque vous frappez ARG_MAX. for f in *ne sera pas, car ARG_MAXs'applique à exec, pas intégré.
  3. while read fpeut rompre avec les noms de fichiers commençant et se terminant par des espaces, qui seront supprimés. Vous pouvez surmonter cela avec while readson paramètre par défaut REPLY, mais cela ne vous aidera toujours pas en ce qui concerne les noms de fichiers avec des retours à la ligne.

B echo.. Personne ne fera cela juste pour faire écho au nom du fichier. Si vous le souhaitez, effectuez l'une des opérations suivantes:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

Le tuyau menant à la whileboucle crée ici un sous-shell implicite qui se ferme à la fin de la boucle, ce qui peut ne pas être intuitif pour certains.

Pour répondre à la question, voici les résultats dans un de mes répertoires contenant 184 fichiers et répertoires.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s
kojiro
la source
Je ne suis pas d'accord avec la déclaration selon laquelle la boucle while génère un sous-shell - au pire des cas, un nouveau thread: ce qui suit essaie de montrer avant et après, des excuses pour le mauvais formatage$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Phil
Techniquement, j'ai mal parlé: le tuyau provoque la sous- couche implicite, pas la boucle while. Je vais éditer.
kojiro
2

find *ne fonctionnera pas correctement si *produit des jetons qui ressemblent à des prédicats plutôt qu'à des chemins.

Vous ne pouvez pas utiliser l' --argument habituel pour résoudre ce problème car il --indique la fin des options et les options de find précèdent les chemins.

Pour résoudre ce problème, vous pouvez utiliser à la find ./*place. Mais cela ne produit pas exactement les mêmes chaînes que for x in *.

Notez que find ./* -prune | while read f ..n'utilise pas réellement la fonctionnalité de numérisation de find. C'est la syntaxe de globbing ./*qui traverse réellement le répertoire et génère des noms. Ensuite, le findprogramme devra effectuer au moins une statvérification sur chacun de ces noms. Vous avez la charge de lancer le programme et de lui faire accéder à ces fichiers, puis de faire des E / S pour lire sa sortie.

Il est difficile d'imaginer comment cela pourrait être tout sauf moins efficace que for x in ./* ....

Kaz
la source
1

Bien pour commencer, forun mot-clé shell, intégré à Bash, findest un exécutable distinct.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

La forboucle ne trouvera les fichiers du caractère globstar que lorsqu'elle se développe, elle ne récursive pas dans les répertoires qu'elle trouve.

Find, d'autre part, recevra également une liste développée par le globstar, mais il trouvera récursivement tous les fichiers et répertoires sous cette liste développée et les dirigera chacun vers la whileboucle.

Ces deux approches peuvent être considérées comme dangereuses dans le sens où elles ne gèrent pas les chemins ou les noms de fichiers contenant des espaces.

C'est à peu près tout ce que je peux penser de commenter ces 2 approches.

slm
la source
J'ai ajouté -prune à la commande find, donc ils se ressemblent davantage.
rubo77
0

Si tous les fichiers retournés par find peuvent être traités par une seule commande (évidemment non applicable à votre exemple d'écho ci-dessus), vous pouvez utiliser xargs:

find * |xargs some-command
Rob
la source
0

Depuis des années, j'utilise ceci: -

find . -name 'filename'|xargs grep 'pattern'|more

pour rechercher certains fichiers (par exemple * .txt) qui contiennent un modèle que grep peut rechercher et le diriger vers plus afin qu'il ne défile pas hors de l'écran. Parfois j'utilise le >> pipe pour écrire les résultats dans un autre fichier que je pourrai regarder plus tard.

Voici un échantillon du résultat: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
Allen
la source