Warum ist das Schleifen über Finds Ausgabe eine schlechte Übung?

170

Diese Frage ist inspiriert von

Warum wird die Verwendung einer Shell-Schleife zum Verarbeiten von Text als schlechte Praxis angesehen?

Ich sehe diese Konstrukte

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

und

for dir in $(find . -type d -name ...); do smth with ${dir}; done

wobei hier fast auf einer täglichen Basis verwendet , auch wenn einige Leute die Zeit nehmen , auf diesen Beiträgen kommentieren zu erklären , warum diese Art von Sachen vermieden werden sollte ...
Sehen die Anzahl solcher Stellen (und die Tatsache , dass manchmal diese Kommentare werden einfach ignoriert) Ich dachte, ich könnte genauso gut eine Frage stellen:

Warum ist das Schleifen findder Ausgabe eine schlechte Übung und wie können Sie einen oder mehrere Befehle für jeden Dateinamen / Pfad ausführen, der von zurückgegeben wird find?

don_crissti
quelle
12
Ich denke, das ist so etwas wie "Niemals ls Ausgabe analysieren!" - Sie können natürlich auch eins nach dem anderen machen, aber sie sind eher ein schneller Hacker als die Qualität der Produktion. Oder, allgemeiner gesagt, definitiv niemals dogmatisch sein.
Bruce Ediger
Dies sollte in eine kanonische Antwort umgewandelt werden
Zaid
6
Weil der Punkt des Findens darin besteht, eine Schleife über das zu machen, was es findet.
OrangeDog
2
Ein zusätzlicher Punkt: Möglicherweise möchten Sie die Ausgabe an eine Datei senden und später im Skript verarbeiten. Auf diese Weise kann die Dateiliste überprüft werden, wenn Sie das Skript debuggen müssen.
user117529

Antworten:

87

Das Problem

for f in $(find .)

kombiniert zwei inkompatible Dinge.

findGibt eine Liste der Dateipfade aus, die durch Zeilenumbrüche begrenzt sind. Während der split + glob-Operator, der aufgerufen wird, wenn Sie diesen $(find .)in diesem $IFSListenkontext nicht zitierten Operator verwenden, ihn in die Zeichen von (standardmäßig Newline, aber auch Leerzeichen und Tabulatorzeichen (und NUL in zsh)) aufteilt und mit jedem resultierenden Wort (mit Ausnahme von) ein Globen ausführt in zsh) (und sogar die Erweiterung in ksh93- oder pdksh-Derivaten abgleichen!).

Auch wenn du es schaffst:

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

Das ist immer noch falsch, da das Newline-Zeichen genauso gültig ist wie jedes andere in einem Dateipfad. Die Ausgabe von find -printist einfach nicht zuverlässig nachbearbeitbar (außer mit einem verschlungenen Trick, wie hier gezeigt ).

Das bedeutet auch, dass die Shell die Ausgabe von findvollständig speichern und dann + glob aufteilen muss (was impliziert, dass diese Ausgabe ein zweites Mal im Speicher gespeichert wird), bevor eine Schleife über die Dateien gestartet wird.

Beachten Sie, dass find . | xargs cmdähnliche Probleme auftreten (Leerzeichen, Zeilenumbrüche, einfache Anführungszeichen, doppelte Anführungszeichen und umgekehrte Schrägstriche (und bei einigen xargImplementierungen sind Bytes, die nicht Teil gültiger Zeichen sind), ein Problem.)

Richtigere Alternativen

Die einzige Möglichkeit, eine forSchleife für die Ausgabe von findzu verwenden zsh, ist die Verwendung von IFS=$'\0'und:

IFS=$'\0'
for f in $(find . -print0)

(Ersetzen -print0durch -exec printf '%s\0' {} +für findImplementierungen, die nicht den Standard unterstützen (aber heutzutage durchaus üblich) -print0).

Hier ist der richtige und tragbare Weg zu verwenden -exec:

find . -exec something with {} \;

Oder wenn somethingSie mehr als ein Argument annehmen können:

find . -exec something with {} +

Wenn Sie diese Liste von Dateien benötigen, die von einer Shell verarbeitet werden sollen:

find . -exec sh -c '
  for file do
    something < "$file"
  done' find-sh {} +

(Vorsicht, es können mehrere gestartet werden sh).

Auf einigen Systemen können Sie Folgendes verwenden:

find . -print0 | xargs -r0 something with

aber , dass wenig Vorteil gegenüber der Standard - Syntax und Mittel somethingsind stdinentweder das Rohr oder die /dev/null.

Ein Grund dafür könnte sein, dass Sie die -POption GNU xargsfür die parallele Verarbeitung verwenden. Das stdinProblem kann auch mit GNU umgangen werden, xargsmit der -aOption, dass Shells die Prozessersetzung unterstützen:

xargs -r0n 20 -P 4 -a <(find . -print0) something

Zum Beispiel, um bis zu 4 gleichzeitige Aufrufe von somethingjeweils 20 Dateiargumenten auszuführen .

Mit zshoder bashkönnen Sie die Ausgabe von auf find -print0folgende Weise durchlaufen :

while IFS= read -rd '' file <&3; do
  something "$file" 3<&-
done 3< <(find . -print0)

read -d '' Liest NUL-getrennte Datensätze anstelle von Zeilenumbrüchen.

bash-4.4und darüber können auch Dateien gespeichert werden, die von find -print0in einem Array zurückgegeben wurden mit:

readarray -td '' files < <(find . -print0)

Das zshÄquivalent (das den Vorteil hat, den findAusgangsstatus beizubehalten):

files=(${(0)"$(find . -print0)"})

Mit zshkönnen Sie die meisten findAusdrücke in eine Kombination aus rekursivem Globbing und Glob-Qualifikationsmerkmalen übersetzen. Eine Schleife find . -name '*.txt' -type f -mtime -1wäre zum Beispiel:

for file (./**/*.txt(ND.m-1)) cmd $file

Oder

for file (**/*.txt(ND.m-1)) cmd -- $file

(Vorsicht : die Notwendigkeit , --wie bei **/*, Dateipfade beginnen , nicht ./, so kann mit beginnen -zum Beispiel).

ksh93und bashschließlich hinzugefügt Unterstützung für **/(wenn auch nicht mehr fortgeschrittene Formen des rekursiven Globbing), aber immer noch nicht die Glob-Qualifikatoren, die die Verwendung von dort **sehr begrenzt macht. Beachten Sie auch, dass bashvor 4.3 beim Abstieg in den Verzeichnisbaum Symlinks folgen.

Wie beim Loop-Over bedeutet dies auch $(find .), dass die gesamte Liste der Dateien in Speicher 1 abgelegt wird . Dies kann jedoch in einigen Fällen wünschenswert sein, wenn Sie nicht möchten, dass Ihre Aktionen für die Dateien einen Einfluss auf die Suche nach Dateien haben (z. B. wenn Sie weitere Dateien hinzufügen, die möglicherweise selbst gefunden werden).

Sonstige Überlegungen zur Zuverlässigkeit / Sicherheit

Rennbedingungen

Wenn wir jetzt von Zuverlässigkeit sprechen, müssen wir die Rennbedingungen zwischen dem Zeitpunkt find/ dem Auffindenzsh einer Datei erwähnen und prüfen , ob sie den Kriterien und dem Zeitpunkt, zu dem sie verwendet wird, entspricht ( TOCTOU-Rennen ).

Selbst wenn man einen Verzeichnisbaum herunterfährt, muss man darauf achten, dass man Symlinks nicht folgt und das ohne TOCTOU-Rennen. find(GNU findzumindest) tut dem durch die Verzeichnisse Öffnen mit openat()mit den richtigen O_NOFOLLOWFlags (sofern unterstützt) und eine Dateibeschreibung für jedes Verzeichnis offen zu halten, zsh/ bash/ kshtu das nicht. Wenn ein Angreifer also in der Lage ist, ein Verzeichnis zum richtigen Zeitpunkt durch einen Symlink zu ersetzen, kann dies dazu führen, dass das falsche Verzeichnis gefunden wird.

Selbst wenn finddas Verzeichnis ordnungsgemäß heruntergefahren wird, mit -exec cmd {} \;und noch mehr mit -exec cmd {} +, wenn cmdes einmal ausgeführt wird, zum Beispiel wenn cmd ./foo/baroder cmd ./foo/bar ./foo/bar/bazwenn die Zeit davon cmdGebrauch macht ./foo/bar, barerfüllen die Attribute von möglicherweise nicht mehr die Kriterien, die mit übereinstimmen find, aber noch schlimmer ./foosind ersetzt durch einen Symlink zu einem anderen Ort (und das Rennfenster ist viel größer, -exec {} +da darauf findgewartet wird, dass genügend Dateien zum Aufrufen vorhanden sind cmd).

Einige findImplementierungen haben ein (noch nicht standardmäßiges) -execdirPrädikat, um das zweite Problem zu lösen.

Mit:

find . -execdir cmd -- {} \;

find chdir()s in das übergeordnete Verzeichnis der Datei, bevor Sie sie ausführen cmd. Anstatt aufzurufen cmd -- ./foo/bar, ruft es cmd -- ./bar( cmd -- barbei einigen Implementierungen, daher das --) auf, sodass das Problem ./foovermieden wird, in einen Symlink geändert zu werden. Das macht die Verwendung von Befehlen rmsicherer (es könnte immer noch eine andere Datei entfernen, aber keine Datei in einem anderen Verzeichnis), aber keine Befehle, die die Dateien möglicherweise ändern, es sei denn, sie wurden so konzipiert, dass sie Symlinks nicht folgen.

-execdir cmd -- {} +manchmal funktioniert es auch, aber mit mehreren Implementierungen, einschließlich einiger Versionen von GNU find, ist es äquivalent zu -execdir cmd -- {} \;.

-execdir hat auch den Vorteil, einige der Probleme zu umgehen, die mit zu tiefen Verzeichnisbäumen verbunden sind.

Im:

find . -exec cmd {} \;

Die Größe des angegebenen Pfads cmdnimmt mit der Tiefe des Verzeichnisses zu, in dem sich die Datei befindet. Wenn diese Größe größer wird als PATH_MAX(etwa 4 KB unter Linux), cmdschlägt jeder Systemaufruf fehl , der auf diesem Pfad ausgeführt wird ENAMETOOLONG.

Mit -execdirwird nur der Dateiname (ggf. vorangestellt ./) übergeben cmd. Die Dateinamen selbst haben auf den meisten Dateisystemen eine viel niedrigere Grenze ( NAME_MAX) als PATH_MAX, sodass der ENAMETOOLONGFehler mit geringerer Wahrscheinlichkeit auftritt.

Bytes vs Zeichen

Außerdem wird bei der Betrachtung der Sicherheit findund allgemeiner beim Umgang mit Dateinamen im Allgemeinen häufig die Tatsache übersehen , dass Dateinamen auf den meisten Unix-ähnlichen Systemen Folgen von Bytes sind (jeder Byte-Wert außer 0 in einem Dateipfad und auf den meisten Systemen). ASCII-basierte, wir werden die seltenen EBCDIC-basierten vorerst ignorieren. 0x2f ist der Pfadbegrenzer.

Es liegt an den Anwendungen, zu entscheiden, ob sie diese Bytes als Text betrachten möchten. Und das tun sie im Allgemeinen, aber im Allgemeinen erfolgt die Übersetzung von Bytes in Zeichen basierend auf dem Gebietsschema des Benutzers, basierend auf der Umgebung.

Dies bedeutet, dass ein gegebener Dateiname je nach Gebietsschema unterschiedliche Textdarstellungen haben kann. Die Bytesequenz 63 f4 74 e9 2e 74 78 74würde beispielsweise côté.txtfür eine Anwendung gelten, die diesen Dateinamen in einem Gebietsschema interpretiert, in dem der Zeichensatz ISO-8859-1 lautet, und cєtщ.txtin einem Gebietsschema, in dem der Zeichensatz stattdessen IS0-8859-5 lautet.

Schlechter. In einem Gebietsschema, in dem der Zeichensatz UTF-8 ist (die heutige Norm), konnten 63 f4 74 e9 2e 74 78 74 einfach keinen Zeichen zugeordnet werden!

findist eine solche Anwendung, die Dateinamen als Text für ihre -name/ -pathPrädikate betrachtet (und mehr, wie -inameoder -regexmit einigen Implementierungen).

Was das bedeutet, ist das zum Beispiel mit mehreren findImplementierungen (einschließlich GNU find).

find . -name '*.txt'

würde unsere 63 f4 74 e9 2e 74 78 74obige Datei nicht finden, wenn sie in einem UTF-8-Gebietsschema aufgerufen wird, da *(das mit 0 oder mehr Zeichen übereinstimmt, nicht mit Bytes) nicht mit diesen Nicht-Zeichen übereinstimmen könnte.

LC_ALL=C find... würde das Problem umgehen, da das Gebietsschema C ein Byte pro Zeichen impliziert und (im Allgemeinen) garantiert, dass alle Bytewerte einem Zeichen zugeordnet sind (obwohl möglicherweise undefinierte für einige Bytewerte).

Wenn es nun darum geht, diese Dateinamen von einer Shell zu durchlaufen, kann dieses Byte gegen das Zeichen ebenfalls ein Problem werden. Wir sehen in der Regel vier Haupttypen von Muscheln in dieser Hinsicht:

  1. Diejenigen, die noch nicht Multibyte-fähig sind, mögen dash. Für sie ist ein Byte einem Zeichen zugeordnet. In UTF-8 sind das côtébeispielsweise 4 Zeichen, aber 6 Bytes. In einem Gebietsschema, in dem UTF-8 der Zeichensatz ist, in

    find . -name '????' -exec dash -c '
      name=${1##*/}; echo "${#name}"' sh {} \;
    

    findfindet erfolgreich die Dateien, deren Name aus 4 in UTF-8 codierten Zeichen besteht, gibt jedoch dashLängen zwischen 4 und 24 aus.

  2. yash: das Gegenteil. Es geht nur um Charaktere . Alle Eingaben werden intern in Zeichen übersetzt. Dies sorgt für die konsistenteste Shell, bedeutet aber auch, dass keine willkürlichen Byte-Sequenzen verarbeitet werden können (solche, die sich nicht in gültige Zeichen übersetzen lassen). Selbst im Gebietsschema C können keine Bytewerte über 0x7f verarbeitet werden.

    find . -exec yash -c 'echo "$1"' sh {} \;
    

    in einem UTF-8-Gebietsschema schlägt beispielsweise auf unserer ISO-8859-1 côté.txtvon früher fehl .

  3. Solche wie bashoder zshwo die Multi-Byte-Unterstützung nach und nach hinzugefügt wurde. Diese werden auf die Berücksichtigung von Bytes zurückgreifen, die nicht wie Zeichen auf Zeichen abgebildet werden können. Hier und da gibt es immer noch ein paar Fehler, insbesondere bei weniger gebräuchlichen Multi-Byte-Zeichensätzen wie GBK oder BIG5-HKSCS (die ziemlich unangenehm sind, da viele ihrer Multi-Byte-Zeichen Bytes im Bereich 0-127 enthalten (wie die ASCII-Zeichen). ).

  4. Diejenigen wie die shvon FreeBSD (mindestens 11) oder mksh -o utf8-modedie Multi-Bytes unterstützen, aber nur für UTF-8.

Anmerkungen

1 Der Vollständigkeit halber können wir einen Hacky-In-Weg erwähnen zsh, um Dateien mithilfe von rekursivem Globbing zu durchlaufen, ohne die gesamte Liste im Speicher zu speichern:

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdist ein Glob-Qualifizierer, der cmd(normalerweise eine Funktion) mit dem aktuellen Dateipfad in aufruft $REPLY. Die Funktion gibt true oder false zurück, um zu entscheiden, ob die Datei ausgewählt werden soll (und kann auch $REPLYmehrere Dateien in einem $replyArray ändern oder zurückgeben ). Hier führen wir die Verarbeitung in dieser Funktion durch und geben false zurück, damit die Datei nicht ausgewählt wird.

Stéphane Chazelas
quelle
Wenn zsh und bash verfügbar sind, ist es möglicherweise besser, nur Globbing- und Shell-Konstrukte zu verwenden, anstatt zu versuchen, sich in ein sicheres findVerhalten zu verwandeln . Globbing ist standardmäßig sicher, während find standardmäßig unsicher ist.
Kevin
@ Kevin, siehe Bearbeiten.
Stéphane Chazelas
182

Warum ist Schleifen über finddie Ausgabe schlechte Praxis?

Die einfache Antwort lautet:

Weil Dateinamen beliebige Zeichen enthalten können .

Daher gibt es kein druckbares Zeichen, mit dem Sie Dateinamen zuverlässig abgrenzen können.


Zeilenumbrüche werden häufig (fälschlicherweise) zum Abgrenzen von Dateinamen verwendet, da es ungewöhnlich ist , Zeilenumbrüche in Dateinamen aufzunehmen.

Wenn Sie Ihre Software jedoch auf willkürlichen Annahmen aufbauen, können Sie im besten Fall nicht mit ungewöhnlichen Fällen umgehen und sind im schlimmsten Fall böswilligen Exploits ausgesetzt, die die Kontrolle über Ihr System verlieren. Es geht also um Robustheit und Sicherheit.

Wenn Sie Software auf zwei verschiedene Arten schreiben können und eine von ihnen Randfälle (ungewöhnliche Eingaben) korrekt behandelt, die andere jedoch besser lesbar ist, könnten Sie argumentieren, dass ein Kompromiss besteht. (Ich würde nicht. Ich bevorzuge richtigen Code.)

Wenn jedoch die richtige, robuste Version des Codes auch leicht zu lesen ist, gibt es keine Entschuldigung für das Schreiben von Code, der in Randfällen fehlschlägt. Dies ist der Fall findund die Notwendigkeit, einen Befehl für jede gefundene Datei auszuführen.


Lassen Sie uns genauer sein: Auf einem UNIX- oder Linux-System können Dateinamen mit Ausnahme von a /(das als Pfadkomponententrennzeichen verwendet wird) beliebige Zeichen enthalten und dürfen kein Null-Byte enthalten.

Ein Null-Byte ist daher der einzig richtige Weg, um Dateinamen zu begrenzen.


Da GNU findeine -print0Primärdatei enthält, die ein Null-Byte zur Begrenzung der ausgegebenen Dateinamen verwendet, find kann GNU problemlos mit GNU xargsund seinem -0Flag (und -rFlag) verwendet werden, um die Ausgabe von find:

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

Es gibt jedoch keinen guten Grund , dieses Formular zu verwenden, weil:

  1. Es fügt eine Abhängigkeit von GNU-Findutils hinzu, die nicht vorhanden sein müssen, und
  2. findist so konzipiert , dass Befehle für die gefundenen Dateien ausgeführt werden können.

Außerdem xargsbenötigt GNU -0und -r, während FreeBSD xargsnur benötigt -0(und keine -rOption hat) und einige xargsüberhaupt nicht unterstützen -0. Halten Sie sich also am besten an die POSIX-Funktionen von find(siehe nächster Abschnitt) und überspringen Sie diese xargs.

Was die findFähigkeit von Punkt 2 betrifft, Befehle für die gefundenen Dateien auszuführen, so hat Mike Loukides das Beste gesagt:

findDas Unternehmen wertet Ausdrücke aus und sucht nicht nach Dateien. Ja, findet auf findjeden Fall Dateien; aber das ist wirklich nur ein nebeneffekt.

--Unix Power Tools


POSIX spezifizierte Verwendungen von find

Wie können Sie einen oder mehrere Befehle für jedes der findErgebnisse ausführen ?

Um einen einzelnen Befehl für jede gefundene Datei auszuführen, verwenden Sie:

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

Um mehrere Befehle nacheinander für jede gefundene Datei auszuführen, wobei der zweite Befehl nur ausgeführt werden sollte, wenn der erste Befehl erfolgreich ist, verwenden Sie:

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

So führen Sie einen einzelnen Befehl für mehrere Dateien gleichzeitig aus:

find dirname ... -exec somecommand {} +

find in Kombination mit sh

Wenn Sie im Befehl Shell- Funktionen verwenden müssen, z. B. die Ausgabe umleiten oder eine Erweiterung vom Dateinamen oder Ähnlichem entfernen möchten, können Sie das sh -cKonstrukt verwenden. Sie sollten ein paar Dinge darüber wissen:

  • Niemals{} direkt in den shCode einbetten . Dies ermöglicht die Ausführung von willkürlichem Code aus in böswilliger Absicht erstellten Dateinamen. Außerdem wird von POSIX nicht einmal spezifiziert, dass es überhaupt funktioniert. (Siehe nächster Punkt.)

  • Verwenden Sie es nicht {}mehrmals oder als Teil eines längeren Arguments. Dies ist nicht portabel. Tun Sie dies zum Beispiel nicht:

    find ... -exec cp {} somedir/{}.bak \;

    So zitieren Sie die POSIX-Spezifikationen fürfind :

    Wenn ein Dienstprogrammname oder eine Argumentzeichenfolge die beiden Zeichen "{}" enthält, aber nicht nur die beiden Zeichen "{}", wird durch die Implementierung festgelegt, ob find diese beiden Zeichen ersetzt oder die Zeichenfolge unverändert verwendet.

    ... Wenn mehr als ein Argument mit den beiden Zeichen "{}" vorhanden ist, ist das Verhalten nicht angegeben.

  • Die Argumente, die auf die an die -cOption übergebene Shell-Befehlszeichenfolge folgen , werden beginnend mit$0 auf die Positionsparameter der Shell gesetzt . Nicht anfangen mit $1.

    Aus diesem Grund empfiehlt es sich, einen "Dummy" $0-Wert einzufügen find-sh, der beispielsweise für die Fehlerberichterstattung in der erstellten Shell verwendet wird. Dies ermöglicht auch die Verwendung von Konstrukten, z. B. "$@"wenn mehrere Dateien an die Shell übergeben werden. Wenn Sie jedoch einen Wert für weglassen $0, wird die zuerst übergebene Datei auf gesetzt $0und somit nicht in die Shell aufgenommen "$@".


Verwenden Sie zum Ausführen eines einzelnen Shell-Befehls pro Datei Folgendes:

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

Es ist jedoch in der Regel besser, die Dateien in einer Shell-Schleife zu verarbeiten, damit Sie nicht für jede einzelne gefundene Datei eine Shell erzeugen:

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

(Beachten Sie, dass for f dodies for f in "$@"; doden einzelnen Positionsparametern entspricht und mit ihnen umgeht. Mit anderen Worten, es werden alle Dateien verwendet, die von gefunden wurden find, unabhängig von Sonderzeichen in ihren Namen.)


Weitere Beispiele für die korrekte findVerwendung:

(Hinweis: Sie können diese Liste jederzeit erweitern.)

Platzhalter
quelle
5
Es gibt einen Fall, in dem ich keine Alternative zur Parsing find-Ausgabe kenne - wo Sie Befehle in der aktuellen Shell ausführen müssen (z. B. weil Sie Variablen festlegen möchten) für jede Datei. In diesem Fall while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)ist das die beste Sprache, die ich kenne. Anmerkungen: <( )ist nicht portabel - verwende bash oder zsh. Auch das -u3und 3<gibt es für den Fall, dass irgendetwas in der Schleife versucht, stdin zu lesen.
Gordon Davisson
1
@GordonDavisson, vielleicht aber was brauchen Sie diese Variablen setzen für ? Ich würde argumentieren, dass alles, was es ist, innerhalb des find ... -execAnrufs behandelt werden sollte. Oder verwenden Sie einfach ein Shell-Glob, wenn es Ihren Anwendungsfall behandelt.
Wildcard
1
Ich möchte oft eine Zusammenfassung nach der Verarbeitung von Dateien drucken ("2 konvertiert, 3 übersprungen, die folgenden Dateien hatten Fehler: ..."), und diese Zählungen / Listen müssen in Shell-Variablen akkumuliert werden. Es gibt auch Situationen, in denen ich ein Array von Dateinamen erstellen möchte, damit ich komplexere Dinge tun kann, als sie der Reihe nach zu iterieren (in diesem Fall ist es das filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson
3
Deine Antwort ist richtig. Allerdings mag ich das Dogma nicht. Auch wenn ich es besser kenne, gibt es viele (besonders interaktive) Anwendungsfälle, in denen es sicher und einfach ist, Schleifen über die findAusgabe zu schreiben oder sogar schlechter zu verwenden ls. Ich mache das täglich ohne Probleme. Ich kenne die Optionen -print0, --null, -z oder -0 für alle Arten von Werkzeugen. Aber ich würde keine Zeit damit verschwenden, sie in meiner interaktiven Shell-Eingabeaufforderung zu verwenden, es sei denn, dies wird wirklich benötigt. Dies könnte auch in Ihrer Antwort vermerkt sein.
Rudimeier
16
@rudimeier, das Argument über Dogma vs. Best Practice wurde bereits zu Tode gebracht . Nicht interessiert. Wenn Sie es interaktiv verwenden und es funktioniert, ist es in Ordnung, gut für Sie - aber ich werde nicht dafür werben, dies zu tun. Der Prozentsatz der Skriptautoren, die sich die Mühe machen, zu lernen, was robuster Code ist, und dies dann nur beim Schreiben von Produktionsskripten tun , anstatt nur das zu tun, was sie gewohnt sind, interaktiv zu tun , ist äußerst gering. Der Umgang ist die ständige Förderung von Best Practices. Die Menschen müssen lernen, dass es eine richtige Art und Weise gibt, Dinge zu tun.
Wildcard
10

Diese Antwort bezieht sich auf sehr große Ergebnismengen und betrifft hauptsächlich die Leistung, z. B. beim Abrufen einer Liste von Dateien über ein langsames Netzwerk. Für kleine Mengen von Dateien (sagen wir einige 100 oder vielleicht sogar 1000 auf einer lokalen Festplatte) ist das meiste umstritten.

Parallelität und Speichernutzung

Abgesehen von den anderen gegebenen Antworten, die sich auf Trennungsprobleme und dergleichen beziehen, gibt es ein weiteres Problem mit

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

Der Teil innerhalb der Backticks muss zuerst vollständig ausgewertet werden, bevor er auf die Zeilenumbrüche aufgeteilt wird. Wenn Sie also eine große Anzahl von Dateien erhalten, kann dies dazu führen, dass die Größe der verschiedenen Komponenten begrenzt wird. Wenn es keine Grenzen gibt, ist möglicherweise kein Speicher mehr verfügbar. und in jedem Fall müssen Sie warten, bis die gesamte Liste von ausgegeben findund dann von analysiert wurde, forbevor Sie überhaupt Ihre erste ausführen smth.

Die bevorzugte Unix-Methode besteht darin, mit parallel laufenden Pipes zu arbeiten, die im Allgemeinen auch keine willkürlich großen Puffer benötigen. Das heißt: Sie würden es vorziehen, wenn das Programm findparallel zu Ihrem ausgeführt wird smth, und den aktuellen Dateinamen nur im RAM belassen, während das Programm dies übergibt smth.

Eine zumindest teilweise okische Lösung dafür ist die vorgenannte find -exec smth. Sie müssen nicht mehr alle Dateinamen im Speicher behalten und werden problemlos parallel ausgeführt. Leider startet es auch einen smthProzess pro Datei. Wenn smthnur eine Datei bearbeitet werden kann, muss das so sein.

Die optimale Lösung wäre find -print0 | smth, wenn überhaupt möglich, smthDateinamen auf der STDIN zu verarbeiten. Dann haben Sie nur einen smthProzess, egal wie viele Dateien es gibt, und Sie müssen nur eine kleine Menge von Bytes (unabhängig von der Pipe-Pufferung) zwischen den beiden Prozessen puffern. Dies ist natürlich ziemlich unrealistisch, wenn smthes sich um einen Standard-Unix / POSIX-Befehl handelt, kann aber ein Ansatz sein, wenn Sie ihn selbst schreiben.

Wenn dies nicht möglich find -print0 | xargs -0 smthist, ist dies wahrscheinlich eine der besseren Lösungen. Wie in den Kommentaren unter @ dave_thompson_085 erwähnt, xargswerden die Argumente auf mehrere Durchläufe aufgeteilt, smthwenn Systemgrenzen erreicht werden (standardmäßig im Bereich von 128 KB oder in einem vom System vorgegebenen Bereich exec), und es stehen Optionen zur Verfügung , um die Anzahl zu beeinflussen Dateien werden an einen Aufruf von übergeben smth, wodurch ein Gleichgewicht zwischen Anzahl der smthProzesse und anfänglicher Verzögerung gefunden wird.

BEARBEITEN: die Begriffe "am besten" entfernt - es ist schwer zu sagen, ob etwas Besseres auftaucht. ;)

AnoE
quelle
find ... -exec smth {} +ist die Lösung.
Wildcard
find -print0 | xargs smthfunktioniert überhaupt nicht, aber find -print0 | xargs -0 smth(Anmerkung -0) oder find | xargs smthwenn Dateinamen keine Anführungszeichen oder Backslashs enthalten, wird einer smthmit so vielen verfügbaren Dateinamen ausgeführt, die in eine Argumentliste passen . Wenn Sie maxargs überschreiten, wird es smthso oft ausgeführt, wie erforderlich, um alle angegebenen Argumente zu verarbeiten (keine Begrenzung). Mit können Sie kleinere 'Chunks' (also etwas frühere Parallelität) setzen -L/--max-lines -n/--max-args -s/--max-chars.
Dave_Thompson_085
2
Siehe auch
Stéphane Chazelas
4

Ein Grund dafür ist, dass Whitespace in den Werken einen Schraubenschlüssel wirft und bewirkt, dass die Datei 'foo bar' als 'foo' und 'bar' ausgewertet wird.

$ 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
$

Funktioniert in Ordnung, wenn stattdessen -exec verwendet wird

$ 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
quelle
Insbesondere, wenn findes eine Option gibt, mit der ein Befehl für jede Datei ausgeführt werden kann, ist dies ohne Weiteres die beste Option.
Centimane
1
Betrachten Sie auch -exec ... {} \;versus-exec ... {} +
thrig
1
Wenn Sie verwenden for file in "$(find . -type f)" und echo "${file}"dann funktioniert es auch mit Leerzeichen, andere Sonderzeichen, die ich denke, mehr Ärger verursachen
Labyrinthe
9
@mazs - nein, das Zitieren macht nicht das, was Sie denken. Versuchen for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";doneSie in einem Verzeichnis mit mehreren Dateien, die (Ihrer Meinung nach) jeden Dateinamen in einer separaten Zeile mit vorangestelltem Text ausgeben sollen name:. Das tut es nicht.
don_crissti
2

Da die Ausgabe eines Befehls eine einzelne Zeichenfolge ist, für die Schleife jedoch ein Array von Zeichenfolgen erforderlich ist. Der Grund, warum es "funktioniert", ist, dass Muscheln die Zeichenfolge in Leerzeichen für Sie aufteilen.

Zweitens, es sei denn, Sie benötigen ein bestimmtes Feature von find, müssen Sie sich darüber im Klaren sein, dass Ihre Shell höchstwahrscheinlich bereits ein rekursives Glob-Muster von selbst erweitern kann und dass es sich zu einem richtigen Array erweitern lässt.

Bash-Beispiel:

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

Gleiche in Fisch:

for i in **
    echo «$i»
end

Wenn Sie die Funktionen von benötigen find, stellen Sie sicher, dass Sie nur auf NUL aufteilen (z. B. die find -print0 | xargs -r0Redewendung).

Fische können NUL-begrenzte Ausgaben durchlaufen. Also das ist eigentlich nicht schlecht:

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

Als letzte wenig gotcha, in vielen Muscheln (nicht Fisch natürlich), werden Schleifen über Befehlsausgabe der Schleife machen eine Subshell (dh Sie keine Variable in irgendeiner Weise einstellen kann , die sichtbar ist , nachdem die Schleife endet), die Niemals was du willst.

user2394284
quelle
@don_crissti Genau. Es funktioniert im Allgemeinen nicht . Ich habe versucht, sarkastisch zu sein, indem ich sagte, dass es "funktioniert" (mit Anführungszeichen).
user2394284
Beachten Sie, dass das rekursive Globbing zshin den frühen 90er Jahren begann (obwohl Sie es dort benötigen würden **/*). fishWie bei früheren Implementierungen von bashs äquivalentem Feature werden beim Abstieg in den Verzeichnisbaum jedoch Symlinks verwendet. Die Unterschiede zwischen den Implementierungen finden Sie unter Das Ergebnis von ls *, ls ** und ls *** .
Stéphane Chazelas
1

Das Durchlaufen der Ausgabe von find ist keine schlechte Übung. In dieser und allen anderen Situationen wird davon ausgegangen, dass Ihre Eingabe ein bestimmtes Format hat, anstatt zu wissen (zu testen und zu bestätigen), dass es ein bestimmtes Format ist.

tldr / cbf: find | parallel stuff

Jan Kyu Peblik
quelle