418 Stimmen

Wie kann man eine Schleife durch die von find zurückgegebenen Dateinamen ziehen?

x=$(find . -name "*.txt")
echo $x

Wenn ich den obigen Code in der Bash-Shell ausführe, erhalte ich eine Zeichenkette mit mehreren durch Leerzeichen getrennten Dateinamen, keine Liste.

Natürlich kann ich sie weiter nach Leerzeichen trennen, um eine Liste zu erhalten, aber ich bin sicher, dass es einen besseren Weg gibt, dies zu tun.

Wie kann man also am besten eine Schleife durch die Ergebnisse einer find Befehl?

693voto

Kevin Punkte 51067

TL;DR: Wenn es Ihnen nur um die korrekteste Antwort geht, wollen Sie wahrscheinlich meine persönliche Präferenz (siehe unten in diesem Beitrag):

# execute `process` once for each file
find . -name '*.txt' -exec process {} \;

Wenn Sie Zeit haben, lesen Sie sich den Rest durch, um verschiedene Möglichkeiten und die Probleme mit den meisten von ihnen zu sehen.


Die vollständige Antwort:

Welcher Weg der beste ist, hängt davon ab, was Sie tun wollen, aber hier sind ein paar Möglichkeiten. Solange keine Datei oder kein Ordner im Teilbaum ein Leerzeichen im Namen hat, können Sie einfach eine Schleife über die Dateien ziehen:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Geringfügig besser, ohne die temporäre Variable x :

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

Es viel Es ist besser, wenn man so oft wie möglich globiert. Leerzeichenfrei, für Dateien im aktuellen Verzeichnis:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Durch die Aktivierung des globstar können Sie alle passenden Dateien in diesem Verzeichnis und allen Unterverzeichnissen globieren:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

In einigen Fällen, z. B. wenn die Dateinamen bereits in einer Datei stehen, müssen Sie möglicherweise read :

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

read kann sicher verwendet werden in Kombination mit find indem Sie das Begrenzungszeichen entsprechend setzen:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Für komplexere Suchvorgänge werden Sie wahrscheinlich die find entweder mit seinem -exec oder mit der Option -print0 | xargs -0 :

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

find Sie können auch in das Verzeichnis der einzelnen Dateien wechseln, bevor Sie einen Befehl ausführen, indem Sie -execdir 代わりに -exec und kann interaktiv gemacht werden (Eingabeaufforderung vor Ausführung des Befehls für jede Datei) mit -ok 代わりに -exec (ou -okdir 代わりに -execdir ).

*: Technisch gesehen, sind beide find y xargs (standardmäßig) führt den Befehl mit so vielen Argumenten aus, wie in die Befehlszeile passen, und zwar so oft, wie es nötig ist, um alle Dateien zu durchsuchen. In der Praxis spielt das keine Rolle, es sei denn, Sie haben eine sehr große Anzahl von Dateien, und wenn Sie die Länge überschreiten, aber alle in der gleichen Befehlszeile benötigen, Sie sind SOL einen anderen Weg finden.

213voto

David W. Punkte 101611

Was auch immer Sie tun, verwenden Sie keine for Schleife :

# Don't do this
for file in $(find . -name "*.txt")
do
    …code using "$file"
done

Drei Gründe:

  • Damit die for-Schleife überhaupt beginnen kann, muss die find muss bis zum Ende laufen.
  • Wenn ein Dateiname Leerzeichen (einschließlich Leerzeichen, Tabulator oder Zeilenumbruch) enthält, wird er als zwei getrennte Namen behandelt.
  • Auch wenn es jetzt unwahrscheinlich ist, können Sie Ihren Kommandozeilenpuffer überlaufen lassen. Stellen Sie sich vor, Ihr Kommandozeilenpuffer fasst 32 KB und Ihr for Schleife gibt 40KB Text zurück. Die letzten 8KB werden direkt aus Ihrer for Schleife und Sie werden es nicht merken.

Verwenden Sie immer eine while read konstruieren:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    …code using "$file"
done

Die Schleife wird ausgeführt, während die find Befehl ausgeführt wird. Außerdem funktioniert dieser Befehl auch, wenn ein Dateiname mit Leerzeichen zurückgegeben wird. Außerdem wird Ihr Befehlszeilenpuffer nicht überlaufen.

En -print0 wird die NULL als Dateitrennzeichen anstelle eines Zeilenumbruchs verwendet und die -d $'\0' wird beim Lesen NULL als Trennzeichen verwendet.

150voto

0xC0000022L Punkte 19207
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Hinweis: Diese Methode et die von bmargulies gezeigte (zweite) Methode kann sicher mit Leerzeichen in den Datei-/Ordnernamen verwendet werden.

Um auch den - etwas exotischen - Fall von Zeilenumbrüchen in den Datei-/Ordnernamen abzudecken, müssen Sie auf die Option -exec Prädikat von find wie diese:

find . -name '*.txt' -exec echo "{}" \;

En {} ist der Platzhalter für das gefundene Element und die \; wird zur Beendigung der -exec Prädikat.

Und der Vollständigkeit halber möchte ich noch eine weitere Variante hinzufügen - man muss die *nix-Wege für ihre Vielseitigkeit lieben:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

Dadurch würden die Drucksachen mit einem \0 Zeichen, das meines Wissens in keinem der Dateisysteme in Datei- oder Ordnernamen erlaubt ist, und sollte daher alle Bereiche abdecken. xargs hebt sie einen nach dem anderen auf und ...

33voto

Michael Brux Punkte 4066

Dateinamen können Leerzeichen und sogar Steuerzeichen enthalten. Leerzeichen sind (standardmäßig) Begrenzungszeichen für die Shell-Expansion in der Bash und als Folge davon x=$(find . -name "*.txt") aus der Frage ist überhaupt nicht zu empfehlen. Wenn find einen Dateinamen mit Leerzeichen erhält, z.B. "the file.txt" erhalten Sie 2 getrennte Strings zur Verarbeitung, wenn Sie x in einer Schleife. Sie können dies verbessern, indem Sie den Begrenzer ändern (bash IFS Variable) z.B. zu \r\n aber Dateinamen können Steuerzeichen enthalten - dies ist also keine (völlig) sichere Methode.

Meiner Ansicht nach gibt es 2 empfohlene (und sichere) Muster für die Verarbeitung von Dateien:

1. Verwenden Sie die for-Schleife und die Erweiterung des Dateinamens:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Finde-lese-weil & Prozess-Substitution verwenden

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Bemerkungen

auf Muster 1:

  1. bash gibt das Suchmuster ("*.txt") zurück, wenn keine passende Datei gefunden wird - daher wird die zusätzliche Zeile "continue, if file does not exist" benötigt. siehe Bash-Handbuch, Dateinamenerweiterung
  2. Shell-Option nullglob kann verwendet werden, um diese zusätzliche Zeile zu vermeiden.
  3. "Wenn die failglob Shell-Option gesetzt ist und keine Übereinstimmungen gefunden werden, wird eine Fehlermeldung ausgegeben und der Befehl wird nicht ausgeführt." (aus dem obigen Bash-Handbuch)
  4. Shell-Option globstar : "Wenn gesetzt, wird das Muster '**', das in einem Dateinamenexpansionskontext verwendet wird, auf alle Dateien und null oder mehr Verzeichnisse und Unterverzeichnisse passen. Wenn das Muster von einem '/' gefolgt wird, passen nur Verzeichnisse und Unterverzeichnisse." siehe Bash Manual, Shopt Builtin
  5. andere Optionen für die Erweiterung des Dateinamens: extglob , nocaseglob , dotglob & Shell-Variable GLOBIGNORE

auf Muster 2:

  1. Dateinamen können Leerzeichen, Tabulatoren, Leerzeichen, Zeilenumbrüche, ... enthalten, um Dateinamen auf sichere Weise zu verarbeiten, find avec -print0 verwendet wird: der Dateiname wird mit allen Steuerzeichen gedruckt und mit NUL abgeschlossen. siehe auch Gnu Findutils Manpage, Behandlung unsicherer Dateinamen , sichere Handhabung von Dateinamen , Ungewöhnliche Zeichen in Dateinamen . Siehe David A. Wheeler (siehe unten) für eine ausführliche Erörterung dieses Themas.

  2. Es gibt einige mögliche Muster für die Verarbeitung von Suchergebnissen in einer while-Schleife. Andere (Kevin, David W.) haben gezeigt, wie man dies mit Pipes machen kann:

    files_found=1
    find . -name "*.txt" -print0 | 
       while IFS= read -r -d '' file; do
           # single filename in $file
           echo "$file"
           files_found=0   # not working example
           # your code here
       done
    [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"

    Wenn Sie dieses Stück Code ausprobieren, werden Sie sehen, dass es nicht funktioniert: files_found ist immer "true" & der Code wird immer echo "keine Dateien gefunden". Der Grund dafür ist, dass jeder Befehl einer Pipeline in einer separaten Subshell ausgeführt wird, so dass die geänderte Variable innerhalb der Schleife (separate Subshell) nicht die Variable im Hauptshellskript ändert. Aus diesem Grund empfehle ich die Verwendung der Prozesssubstitution als das "bessere", nützlichere und allgemeinere Muster.
    Véase Ich setze Variablen in einer Schleife, die sich in einer Pipeline befindet. Warum verschwinden sie... (aus Gregs Bash FAQ) für eine ausführliche Diskussion zu diesem Thema.

Zusätzliche Referenzen und Quellen:

12voto

user569825 Punkte 2349

(Aktualisiert, um @Socowis exzellente Geschwindigkeitsverbesserung zu berücksichtigen)

Mit jedem $SHELL die es unterstützt (dash/zsh/bash...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Erledigt.


Originalantwort (kürzer, aber langsamer):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X