für vs find in Bash

28

Beim Durchlaufen von Dateien gibt es zwei Möglichkeiten:

  1. benutze einen for-loop:

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

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

Angenommen, diese beiden Schleifen finden dieselbe Liste von Dateien. Was sind die Unterschiede zwischen diesen beiden Optionen in Bezug auf Leistung und Handhabung?

rubo77
quelle
1
Warum? findöffnet die gefundenen Dateien nicht. Das einzige, was ich hier in Bezug auf eine große Anzahl von Dateien sehen kann, ist ARG_MAX .
Kojiro
1
read fLesen Sie die Antworten und Kommentare, aus denen hervorgeht, dass Dateinamen beim Lesen beschädigt werden (z. B. Namen mit führenden Leerzeichen). Auch find * -prunescheint einen sehr gewundenen Weg zu sagen , einfach zu sein , ls -1nicht wahr?
Ian D. Allen
4
Gehen Sie nicht davon aus, dass die beiden Schleifen denselben Satz von Dateien finden. In den meisten Fällen wird dies nicht der Fall sein. Auch das sollte find .nicht sein find *.
Alexis
1
@terdon Ja, Parsen ls -list eine schlechte Idee. Aber das Parsen ls -1(das ist 1kein l) ist nicht schlimmer als das Parsen find * -prune. Beide schlagen bei Dateien mit Zeilenumbrüchen im Namen fehl.
Ian D. Allen
5
Ich vermute, dass wir beide mehr Zeit damit verbracht haben, diese Frage und die Antworten zu lesen, als den gesamten Leistungsunterschied über die Lebensdauer des fraglichen Skripts.
Mpez0

Antworten:

9

1.

Der erste:

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

nicht für Dateien genannt -n, -eund Varianten wie -neneund mit einigen bash - Implementierungen mit Dateinamen Schrägstriche enthalten.

Der Zweite:

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

nicht noch mehr Fälle (Dateien genannt !, -H, -name, , (die mit Leerzeichen oder Zeilenumbrüche starten, Dateinamen oder Ende enthalten ...)

Es ist die Shell, die sich erweitert *und findnichts anderes tut, als die Dateien zu drucken, die sie als Argumente erhält. Sie hätten printf '%s\n'stattdessen auch verwenden können, was, wie printfes eingebaut ist, auch den zu vielen Argumenten möglichen Fehler vermeidet .

2.

Die Erweiterung von *ist sortiert, Sie können es etwas schneller machen, wenn Sie die Sortierung nicht benötigen. In zsh:

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

oder einfach:

printf '%s\n' *(oN)

bashSoweit ich das beurteilen kann, gibt es kein Äquivalent, weshalb Sie darauf zurückgreifen müssen find.

3.

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

(oben unter Verwendung einer GNU / BSD- -print0Nicht-Standard-Erweiterung).

Das beinhaltet immer noch das Aufrufen eines Suchbefehls und die Verwendung einer langsamen while readSchleife. Es ist daher wahrscheinlich langsamer als die Verwendung der forSchleife, es sei denn, die Liste der Dateien ist riesig.

4.

Im Gegensatz zur Shell-Platzhaltererweiterung findwird lstatbei jeder Datei ein Systemaufruf ausgeführt, sodass es unwahrscheinlich ist, dass das Nichtsortieren dies kompensiert.

Mit GNU / BSD findkann dies vermieden werden, indem die -maxdepthErweiterung verwendet wird, die eine Optimierung auslöst, bei der Folgendes gespeichert wird lstat:

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

Da finddie Ausgabe von Dateinamen beginnt, sobald sie gefunden wurden (mit Ausnahme der Stdio-Ausgabepufferung), ist es möglicherweise schneller, wenn die Ausführung in der Schleife zeitaufwändig ist und die Liste der Dateinamen mehr als ein Stdio-Puffer ist (4) / 8 kB). In diesem Fall beginnt die Verarbeitung in der Schleife, bevor findalle Dateien gefunden wurden. Auf GNU- und FreeBSD-Systemen kann dies möglicherweise stdbufschneller geschehen (Deaktivieren der Stdio-Pufferung).

5.

Die POSIX / standard / portable-Methode zum Ausführen von Befehlen für jede Datei findbesteht darin, das -execPrädikat zu verwenden:

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

In diesem Fall echoist dies jedoch weniger effizient als das Schleifen in der Shell, da die Shell über eine integrierte Version von echowhile verfügt find, die einen neuen Prozess erzeugen und /bin/echoin jeder Datei ausführen muss.

Wenn Sie mehrere Befehle ausführen müssen, haben Sie folgende Möglichkeiten:

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

Aber Vorsicht, das cmd2wird nur ausgeführt, wenn cmd1es erfolgreich ist.

6.

Eine kanonische Möglichkeit, komplexe Befehle für jede Datei auszuführen, ist der Aufruf einer Shell mit -exec ... {} +:

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

Diesmal sind wir wieder effizient, echoda wir die sheingebaute -exec +Version verwenden und die Version so wenig shwie möglich erscheint.

7.

In meinen Tests mit einem Verzeichnis mit 200.000 Dateien mit Kurznamen auf ext4 ist das Verzeichniszsh (Absatz 2) bei weitem das schnellste, gefolgt von der ersten einfachen for i in *Schleife (obwohl dies wie üblich bashviel langsamer ist als andere Shells).

Stéphane Chazelas
quelle
Was macht der !Befehl find?
Rubo77
@ rubo77, !ist für die Verneinung. ! -name . -prune more...wird für jede Datei aber tun -prune(und gibt more...da -pruneimmer true zurück) .. So wird es more...für alle Dateien in tun ., aber wird ausschließen .und wird nicht in Unterverzeichnisse von absteigen .. Es ist also das Standardäquivalent zu GNUs -mindepth 1 -maxdepth 1.
Stéphane Chazelas
18

Ich habe dies in einem Verzeichnis mit 2259 Einträgen versucht und den timeBefehl verwendet.

Die Ausgabe von time for f in *; do echo "$f"; done(abzüglich der Dateien!) Ist:

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

Die Ausgabe von time find * -prune | while read f; do echo "$f"; done(abzüglich der Dateien!) Ist:

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

Ich habe jeden Befehl mehrmals ausgeführt, um Cache-Fehler zu vermeiden. Dies legt nahe, dass das Speichern in bash(für i in ...) schneller ist als das Verwenden findund Weiterleiten der Ausgabe (an bash).

Der Vollständigkeit halber habe ich die Pipe weggelassen find, da sie in Ihrem Beispiel völlig überflüssig ist. Die Ausgabe von just find * -pruneist:

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

Außerdem time echo *(Ausgabe ist nicht durch Zeilenumbrüche getrennt, leider):

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

An dieser Stelle vermute ich, dass der Grund echo *dafür, dass weniger Zeilenumbrüche ausgegeben werden, eher darin besteht, dass die Ausgabe nicht so stark scrollt. Lass uns testen ...

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

ergibt:

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

während time find * -prune > /dev/nullergibt:

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

und time for f in *; do echo "$f"; done > /dev/nullergibt:

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

und schließlich: time echo * > /dev/nullergibt:

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

Einige der Abweichungen können durch zufällige Faktoren erklärt werden, aber es scheint klar zu sein:

  • Ausgabe ist langsam
  • Rohrleitungen kosten ein bisschen
  • for f in *; do ...ist langsamer als find * -prunefür sich allein, aber für die obigen Konstruktionen mit Rohren ist es schneller.

Abgesehen davon scheinen beide Ansätze Namen mit Leerzeichen gut zu handhaben.

BEARBEITEN:

Timings für 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

Also, zusätzliche Schlussfolgerung:

  • find * -pruneist langsamer als find . -maxdepth 1- im ersten Fall verarbeitet die Shell einen Globus und erstellt dann eine (große) Befehlszeile für find. NB: find . -prunekehrt gerade zurück ..

Weitere Tests time find . -maxdepth 1 -exec echo {} \; >/dev/null:

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

Fazit:

  • langsamster Weg, es so weit zu tun. Wie in den Kommentaren zu der Antwort, in der dieser Ansatz vorgeschlagen wurde, ausgeführt wurde, erzeugt jedes Argument eine Shell.
Phil
quelle
Welches Rohr ist überflüssig? Kannst du die Linie zeigen, die du ohne Pipe benutzt hast?
Rubo77
2
@ rubo77 find * -prune | while read f; do echo "$f"; donehat die redundante Pipe - die Pipe gibt nur genau das aus, was sie selbst findausgibt. Ohne Pipe wäre es einfach. find * -prune Die Pipe ist nur redundant, weil das Ding auf der anderen Seite der Pipe einfach stdin nach stdout kopiert (zum größten Teil). Es ist eine teure No-Op. Wenn Sie etwas mit der Ausgabe von find anfangen wollen, außer es einfach wieder auszuspucken, ist das etwas anderes.
Phil
Vielleicht ist der Hauptzeitaufwand der *. Wie BitsOfNix erklärte: schlage ich nicht vor stark zu verwenden *und .für findstatt.
Rubo77
@ rubo77 scheint so. Das habe ich wohl übersehen. Ich habe Erkenntnisse für mein System hinzugefügt. Ich gehe davon aus, dass dies find . -pruneschneller ist, da findein Verzeichniseintrag wörtlich gelesen wird, während die Shell dies ebenfalls tut und möglicherweise mit dem Glob übereinstimmt (möglicherweise für optimiert *) und dann die große Befehlszeile für erstellt find.
Phil
1
find . -prunedruckt nur .auf meinem System. Es macht fast gar keine Arbeit. Es ist überhaupt nicht dasselbe, als find * -prunedass alle Namen im aktuellen Verzeichnis angezeigt werden. Ein Nackter read fwird Dateinamen mit führenden Leerzeichen entstellen.
Ian D. Allen
10

Ich würde auf jeden Fall mit find gehen, obwohl ich Ihren Fund so ändern würde:

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

In Bezug auf die Leistung findist dies natürlich viel schneller, je nach Ihren Anforderungen. Was Sie gerade dabei haben for, zeigt nur die Dateien / Verzeichnisse im aktuellen Verzeichnis an, nicht jedoch den Inhalt der Verzeichnisse. Wenn Sie find verwenden, wird auch der Inhalt der Unterverzeichnisse angezeigt.

Ich sage, find ist besser, da mit Ihrem zuerst forder *Wille erweitert werden muss und ich befürchte, dass, wenn Sie ein Verzeichnis mit einer großen Menge von Dateien haben, es die Fehlerargumentliste zu lang geben könnte . Gleiches gilt fürfind *

In einem der Systeme, die ich derzeit verwende, gibt es beispielsweise einige Verzeichnisse mit mehr als 2 Millionen Dateien (jeweils <100.000):

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
quelle
Ich fügte hinzu -prune, um die beiden Beispiele gleich zu machen. und ich bevorzuge die Pipe mit while, damit es einfacher ist, mehr Befehle in der Schleife
anzuwenden
Sie können "Zu viele offene Dateien" in
debian
Das Ändern des harten Grenzwerts ist für meinen POV kaum eine angemessene Umgehung. Besonders wenn es um mehr als 2 Millionen Dateien geht. Ohne Abweichung von der Frage ist in einfachen Fällen ein Verzeichnis mit einer Ebene schneller, aber wenn Sie die Datei- / Verzeichnisstruktur ändern, ist die Migration schwieriger. Mit find und seinen zahlreichen Optionen können Sie besser vorbereitet sein. Trotzdem empfehle ich dringend, * und nicht zu verwenden. für stattdessen finden. Es wäre portabler als *, wenn Sie das Hardlimit nicht kontrollieren könnten ...
BitsOfNix
4
Das wird einen Echo-Prozess pro Datei erzeugen (während es sich in der Shell for-Schleife um das eingebaute Echo handelt, das verwendet wird, ohne einen zusätzlichen Prozess zu forken) und in Verzeichnisse absteigen, so dass es viel langsamer wird . Beachten Sie auch, dass es Punktdateien enthalten wird.
Stéphane Chazelas
Sie haben Recht, ich habe die maximale Tiefe 1 hinzugefügt, damit sie nur auf dem aktuellen Niveau bleibt.
BitsOfNix
7
find * -prune | while read f; do 
    echo "$f"
done

ist eine nutzlose Verwendung von " find- Was Sie sagen, ist effektiv". *Finden Sie für jede Datei im Verzeichnis ( ) keine Dateien. Außerdem ist es aus mehreren Gründen nicht sicher:

  • Backslashes in Pfaden werden speziell ohne die -rOption behandelt read. Dies ist kein Problem mit der forSchleife.
  • Zeilenumbrüche in Pfaden würden alle nicht trivialen Funktionen innerhalb der Schleife unterbrechen. Dies ist kein Problem mit der forSchleife.

Es findist schwierig , mit Dateinamen umzugehen. Verwenden Sie daher die forOption loop, wenn immer dies möglich ist. Außerdem ist das Ausführen eines externen Programms wie findim Allgemeinen langsamer als das Ausführen eines internen Schleifenbefehls wie for.

l0b0
quelle
@ I0b0 Was ist mit find -path './*' -prune oder find -path './[^.‹*' -prune (um versteckte Dateien und Verzeichnisse zu vermeiden) als besseres Konstrukt - in voller Form: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
1
Weder find's -print0noch xargs' -0sind POSIX-kompatibel, und Sie können keine willkürlichen Befehle eingeben sh -c ' ... '(einfache Anführungszeichen können nicht in einfache Anführungszeichen eingeschlossen werden), daher ist dies nicht ganz so einfach.
l0b0
4

Aber wir sind Trottel für Leistungsfragen! Diese Versuchsanfrage geht von mindestens zwei Annahmen aus, die sie fürchterlich ungültig machen.

A. Angenommen, sie finden dieselben Dateien…

Nun, sie werden zuerst dieselben Dateien finden, weil sie beide über denselben Glob iterieren, nämlich *. Es find * -prune | while read fweist jedoch einige Fehler auf, die möglicherweise dazu führen, dass nicht alle erwarteten Dateien gefunden werden:

  1. POSIX find akzeptiert garantiert nicht mehr als ein Pfadargument. Die meisten findImplementierungen tun dies, aber Sie sollten sich trotzdem nicht darauf verlassen.
  2. find *kann brechen, wenn Sie schlagen ARG_MAX. for f in *wird nicht, da ARG_MAXgilt exec, nicht eingebaut.
  3. while read fKann mit Dateinamen brechen, die mit Leerzeichen beginnen und enden, die dann entfernt werden. Sie könnten dies mit while readund seinem Standardparameter überwinden REPLY, aber das hilft Ihnen immer noch nicht, wenn es um Dateinamen mit Zeilenumbrüchen geht.

B echo.. Niemand wird dies tun, nur um den Namen der Datei wiederzugeben. Wenn Sie das möchten, führen Sie einfach einen der folgenden Schritte aus:

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

Die Pipe zur whileSchleife erzeugt hier eine implizite Subshell, die sich schließt, wenn die Schleife endet, was für manche uninteressant sein kann.

Um die Frage zu beantworten, sind hier die Ergebnisse in einem Verzeichnis von mir, das 184 Dateien und Verzeichnisse enthält.

$ 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
quelle
Ich bin mit der Aussage nicht einverstanden, dass die while-Schleife eine Subshell erzeugt - im schlimmsten Fall einen neuen Thread: Folgendes versucht, vorher und nachher Entschuldigungen für die schlechte Formatierung anzuzeigen$ 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
Technisch habe ich falsch geschrieben: Die Pipe verursacht die implizite Subshell, nicht die while-Schleife. Ich werde bearbeiten.
Kojiro
2

find *funktioniert nicht richtig, wenn *Tokens erzeugt werden, die eher wie Prädikate als wie Pfade aussehen.

Sie können das Problem nicht mit dem üblichen --Argument beheben, da --es das Ende von Optionen angibt und die Optionen von find vor den Pfaden stehen.

Um dieses Problem zu beheben, können Sie find ./*stattdessen verwenden. Aber dann produziert es nicht genau die gleichen Saiten wie for x in *.

Beachten Sie, dass die find ./* -prune | while read f ..Scanfunktion von nicht verwendet wird find. Es ist die Globbing-Syntax, ./*die das Verzeichnis tatsächlich durchläuft und Namen generiert. Dann muss das findProgramm mindestens statjeden dieser Namen überprüfen. Sie müssen das Programm starten, auf diese Dateien zugreifen und dann die E / A-Vorgänge ausführen, um die Ausgabe zu lesen.

Es ist schwer vorstellbar, wie es alles andere als weniger effizient sein könnte for x in ./* ....

Kaz
quelle
1

Gut für den Anfang forist ein Shell-Schlüsselwort, das in Bash integriert ist, während findes sich um eine separate ausführbare Datei handelt.

$ type -a for
for is a shell keyword

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

Die forSchleife findet die Dateien des Globstar-Zeichens nur, wenn sie erweitert wird. Sie wird nicht in die gefundenen Verzeichnisse zurückgeführt.

Auf der anderen Seite erhält Find auch eine vom Globstar erweiterte Liste, findet jedoch rekursiv alle Dateien und Verzeichnisse unter dieser erweiterten Liste und leitet sie jeweils an die whileSchleife weiter.

Beide Vorgehensweisen können als gefährlich angesehen werden, da sie keine Pfade oder Dateinamen verarbeiten, die Leerzeichen enthalten.

Das ist alles, was ich mir vorstellen kann, diese beiden Ansätze zu kommentieren.

slm
quelle
Ich habe -prune zum find-Befehl hinzugefügt, damit sie sich ähnlicher sind.
Rubo77
0

Wenn alle von find zurückgegebenen Dateien mit einem einzigen Befehl verarbeitet werden können (gilt natürlich nicht für das obige Echo-Beispiel), können Sie xargs verwenden:

find * |xargs some-command
rauben
quelle
0

Ich benutze das seit Jahren: -

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

um nach bestimmten Dateien zu suchen (z. B. * .txt), die ein Muster enthalten, nach dem grep suchen kann, und um es weiterzuleiten, damit es nicht vom Bildschirm rollt. Manchmal benutze ich die >> -Pipe, um die Ergebnisse in eine andere Datei zu schreiben, die ich mir später ansehen kann.

Hier ist ein Beispiel des Ergebnisses:

./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
quelle