Wie durchlaufe ich die von find zurückgegebenen Dateinamen?

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

Wenn ich den obigen Code in der Bash-Shell ausführe, erhalte ich eine Zeichenfolge, die mehrere durch Leerzeichen getrennte Dateinamen enthält, keine Liste.

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

Was ist der beste Weg, um die Ergebnisse eines findBefehls zu durchlaufen ?

Haiyuan Zhang
quelle
3
Der beste Weg, um Dateinamen zu durchlaufen, hängt ziemlich stark davon ab, was Sie tatsächlich damit machen möchten. Wenn Sie jedoch nicht garantieren können , dass keine Dateien Leerzeichen enthalten, ist dies keine gute Möglichkeit, dies zu tun. Was möchten Sie also tun, um die Dateien zu durchlaufen?
Kevin
1
In Bezug auf das Kopfgeld : Die Hauptidee hier ist, eine kanonische Antwort zu erhalten, die alle möglichen Fälle abdeckt (Dateinamen mit neuen Zeilen, problematische Zeichen ...). Die Idee ist, diese Dateinamen dann zu verwenden, um einige Dinge zu erledigen (einen anderen Befehl aufrufen, etwas umbenennen ...). Vielen Dank!
Fedorqui 'SO hör auf zu schaden'
Vergessen Sie nicht, dass eine Datei oder ein Ordnername ".txt" gefolgt von Leerzeichen und einer weiteren Zeichenfolge enthalten kann, z. B. "etwas.txt etwas" oder "etwas.txt"
Yahya Yahyaoui
Verwenden Sie Array, nicht var x=( $(find . -name "*.txt") ); echo "${x[@]}"Dann können Sie durchschleifenfor item in "${x[@]}"; { echo "$item"; }
Ivan

Antworten:

391

TL; DR: Wenn Sie nur hier sind, um die richtigste Antwort zu erhalten, möchten Sie wahrscheinlich meine persönliche Präferenz find . -name '*.txt' -exec process {} \;(siehe unten in diesem Beitrag). Wenn Sie Zeit haben, lesen Sie den Rest durch, um verschiedene Möglichkeiten und die Probleme mit den meisten von ihnen zu sehen.


Die vollständige Antwort:

Der beste Weg hängt davon ab, was Sie tun möchten, aber hier sind einige Optionen. Solange keine Datei oder kein Ordner im Teilbaum Leerzeichen enthält, können Sie die Dateien einfach durchlaufen:

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

Etwas besser, schneiden Sie die temporäre Variable aus x:

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

Es ist viel besser zu globieren, wenn du kannst. White-Space-Safe für Dateien im aktuellen Verzeichnis:

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

Durch Aktivieren der globstarOption können Sie alle übereinstimmenden Dateien in diesem Verzeichnis und in allen Unterverzeichnissen globalisieren:

# 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 sich die Dateinamen bereits in einer Datei befinden, müssen Sie möglicherweise Folgendes verwenden 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

readkann sicher in Kombination mit verwendet findwerden, indem das Trennzeichen entsprechend eingestellt wird:

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

Für komplexere Suchvorgänge möchten Sie wahrscheinlich findentweder die -execOption oder Folgendes verwenden -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

findkann auch in das Verzeichnis jeder Datei kopiert werden, bevor ein Befehl mit -execdiranstelle von ausgeführt wird -exec, und kann interaktiv (Eingabeaufforderung vor Ausführung des Befehls für jede Datei) mit -okanstelle von -exec(oder -okdiranstelle von -execdir) erstellt werden.

*: Technisch gesehen führen beide findund xargs(standardmäßig) den Befehl mit so vielen Argumenten aus, wie sie in die Befehlszeile passen, so oft, bis alle Dateien durchlaufen sind. In der Praxis spielt es keine Rolle, ob Sie über eine sehr große Anzahl von Dateien verfügen. Wenn Sie die Länge überschreiten, diese jedoch alle in derselben Befehlszeile benötigen, finden Sie bei SOL einen anderen Weg.

Kevin
quelle
4
Es ist erwähnenswert, dass in dem Fall done < filenameund die folgenden mit dem Rohr der stdin nicht mehr (→ nicht mehr interaktiven Sachen innerhalb der Schleife) verwendet werden kann, aber in Fällen , wo es gebraucht wird man verwenden kann , 3<anstatt <hinzuzufügen <&3oder -u3zu das readTeil, im Grunde mit einem separaten Dateideskriptor. Ich glaube auch, dass read -d ''es dasselbe ist wie, read -d $'\0'aber ich kann derzeit keine offizielle Dokumentation dazu finden.
Phk
1
für i in * .txt; funktioniert nicht, wenn keine Dateien übereinstimmen. Ein xtra-Test, zB [[-e $ i]], wird benötigt
Michael Brux
2
Ich bin mit diesem Teil verloren: -exec process {} \;und ich vermute, das ist eine ganz andere Frage - was bedeutet das und wie manipuliere ich es? Wo ist ein gutes Q / A oder Doc. drauf?
Alex Hall
1
@AlexHall können Sie immer auf die Manpages ( man find) schauen . In diesem Fall wird -execangewiesen find, den folgenden durch ;(oder +) beendeten Befehl auszuführen, der {}durch den Namen der zu verarbeitenden Datei ersetzt wird (oder, falls +verwendet, durch alle Dateien, die diese Bedingung erfüllt haben).
Kevin
3
@phk -d ''ist besser als -d $'\0'. Letzteres ist nicht nur länger, sondern legt auch nahe, dass Sie Argumente mit Null-Bytes übergeben könnten, dies jedoch nicht. Das erste Null-Byte markiert das Ende der Zeichenfolge. In bash $'a\0bc'ist dasselbe wie aund $'\0'ist dasselbe wie $'\0abc'oder nur die leere Zeichenfolge ''. help readgibt an, dass " das erste Zeichen von delim verwendet wird, um die Eingabe zu beenden ", so dass die Verwendung ''als Trennzeichen ein kleiner Hack ist. Das erste Zeichen in der leeren Zeichenfolge ist das Nullbyte, das immer das Ende der Zeichenfolge markiert (auch wenn Sie es nicht explizit aufschreiben).
Socowi
114

Was auch immer Sie tun, verwenden Sie keine forSchleife :

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

Drei Gründe:

  • Damit die for-Schleife überhaupt startet, findmuss sie vollständig ausgeführt werden.
  • Wenn ein Dateiname ein Leerzeichen (einschließlich Leerzeichen, Tabulator oder Zeilenumbruch) enthält, wird er als zwei separate Namen behandelt.
  • Obwohl dies jetzt unwahrscheinlich ist, können Sie Ihren Befehlszeilenpuffer überlaufen. Stellen Sie sich vor, Ihr Befehlszeilenpuffer enthält for32 KB und Ihre Schleife gibt 40 KB Text zurück. Die letzten 8 KB werden direkt von Ihrer forSchleife entfernt und Sie werden es nie erfahren.

Verwenden Sie immer ein while readKonstrukt:

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

Die Schleife wird ausgeführt, während der findBefehl ausgeführt wird. Außerdem funktioniert dieser Befehl auch dann, wenn ein Dateiname mit Leerzeichen zurückgegeben wird. Und Sie werden Ihren Befehlszeilenpuffer nicht überlaufen lassen.

Der -print0verwendet NULL als Dateitrennzeichen anstelle eines Zeilenumbruchs und -d $'\0'verwendet beim Lesen NULL als Trennzeichen.

David W.
quelle
3
Es funktioniert nicht mit Zeilenumbrüchen in Dateinamen. Verwenden Sie -execstattdessen find's .
Benutzer unbekannt
2
@userunknown - Da hast du recht. -execist am sichersten, da die Shell überhaupt nicht verwendet wird. NL in Dateinamen ist jedoch ziemlich selten. Leerzeichen in Dateinamen sind weit verbreitet. Der Hauptpunkt ist, keine forSchleife zu verwenden , die von vielen Postern empfohlen wird.
David W.
1
@userunknown - Hier. Ich habe dies behoben, sodass jetzt Dateien mit neuen Zeilen, Registerkarten und anderen Leerzeichen behandelt werden. Der springende Punkt des Beitrags ist, das OP anzuweisen, das for file $(find)wegen der damit verbundenen Probleme nicht zu verwenden .
David W.
4
Wenn Sie -exec verwenden können, ist es besser, aber es gibt Zeiten, in denen Sie wirklich den Namen benötigen, der der Shell zurückgegeben wird. Zum Beispiel, wenn Sie Dateierweiterungen entfernen möchten.
Ben Reser
5
Sie sollten die -rOption verwenden, um read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Hinweis: Diese Methode und die (zweite) Methode von bmargulies können sicher mit Leerzeichen in den Datei- / Ordnernamen verwendet werden.

Um auch den - etwas exotischen - Fall von Zeilenumbrüchen in den behandelten Datei- / Ordnernamen zu haben, müssen Sie auf das -execPrädikat findwie folgt zurückgreifen :

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

Das {}ist der Platzhalter für das gefundene Objekt und \;wird zum Beenden des verwendet-exec Prädikats verwendet.

Und der Vollständigkeit halber möchte ich noch eine Variante hinzufügen - Sie müssen die * nix-Methoden wegen ihrer Vielseitigkeit lieben:

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

Dies würde die gedruckten Elemente mit einem \0Zeichen trennen , das meines Wissens in keinem der Dateisysteme in Datei- oder Ordnernamen zulässig ist, und sollte daher alle Grundlagen abdecken. xargsholt sie dann eins nach dem anderen ab ...

0xC0000022L
quelle
3
Schlägt fehl, wenn Zeilenumbruch im Dateinamen.
Benutzer unbekannt
2
@Benutzer unbekannt: Sie haben Recht, es ist ein Fall, an den ich überhaupt nicht gedacht hatte, und das finde ich sehr exotisch. Aber ich habe meine Antwort entsprechend angepasst.
0xC0000022L
5
Wahrscheinlich erwähnenswert, dass find -print0und xargs -0beide GNU - Erweiterungen sind und nicht tragbar (POSIX) Argumente. Unglaublich nützlich auf den Systemen, auf denen sie vorhanden sind!
Toby Speight
1
Dies schlägt auch bei Dateinamen fehl, die Backslashes enthalten (die behoben werden read -rwürden), oder bei Dateinamen, die auf Leerzeichen enden (die behoben IFS= readwürden). Daher BashFAQ # 1 darauf hindeutetwhile IFS= read -r filename; do ...
Charles Duffy
1
Ein weiteres Problem dabei ist, dass es so aussieht, als würde der Hauptteil der Schleife in derselben Shell ausgeführt, aber dies ist exitnicht der Fall , sodass beispielsweise nicht wie erwartet funktioniert und die im Hauptteil der Schleife festgelegten Variablen nach der Schleife nicht verfügbar sind.
EM0
17

Dateinamen können Leerzeichen und sogar Steuerzeichen enthalten. Leerzeichen sind (Standard-) Trennzeichen für die Shell-Erweiterung in Bash und werden daher x=$(find . -name "*.txt")aus der Frage überhaupt nicht empfohlen. Wenn find einen Dateinamen mit Leerzeichen "the file.txt"erhält, erhalten Sie z. B. 2 getrennte Zeichenfolgen zur Verarbeitung, wenn Sie xin einer Schleife verarbeiten. Sie können dies verbessern, indem Sie das Trennzeichen (Bash- IFSVariable) ändern, z. B. in \r\n, Dateinamen können jedoch Steuerzeichen enthalten. Dies ist also keine (vollständig) sichere Methode.

Aus meiner Sicht gibt es zwei empfohlene (und sichere) Muster für die Verarbeitung von Dateien:

1. Verwenden Sie für die Erweiterung von Schleife und Dateinamen:

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. Verwenden Sie find-read-while & process substitution

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

Bemerkungen

zu Muster 1:

  1. bash gibt das Suchmuster ("* .txt") zurück, wenn keine übereinstimmende Datei gefunden wird. Daher wird die zusätzliche Zeile "Weiter, wenn keine Datei vorhanden ist" benötigt. Siehe Bash-Handbuch, Dateinamenerweiterung
  2. Die Shell-Option nullglobkann verwendet werden, um diese zusätzliche Zeile zu vermeiden.
  3. "Wenn die failglobShell-Option festgelegt ist und keine Übereinstimmungen gefunden werden, wird eine Fehlermeldung gedruckt und der Befehl nicht ausgeführt." (aus dem obigen Bash-Handbuch)
  4. Shell-Option globstar: "Wenn festgelegt, stimmt das in einem Dateinamenerweiterungskontext verwendete Muster '**' mit allen Dateien und null oder mehr Verzeichnissen und Unterverzeichnissen überein. Wenn dem Muster ein '/' folgt, stimmen nur Verzeichnisse und Unterverzeichnisse überein." sehen Bash Manual, Shopt Builtin
  5. andere Optionen für die Dateinamen - Erweiterung: extglob, nocaseglob, dotglob& Shell - VariableGLOBIGNORE

zu Muster 2:

  1. Dateinamen können Rohlingen, Tabulatoren, Leerzeichen, Zeilenumbrüche, ... zu verarbeiten Dateinamen in einer sicheren Art und Weise enthalten, findmit -print0verwendet wird: Dateiname wird mit allen Steuerzeichen gedruckt und mit NUL beendet. Siehe auch Gnu Findutils Manpage, Unsichere Behandlung von Dateinamen , sichere Behandlung von Dateinamen , ungewöhnliche Zeichen in Dateinamen . Siehe David A. Wheeler unten für eine detaillierte Diskussion dieses Themas.

  2. Es gibt einige mögliche Muster, um Suchergebnisse in einer while-Schleife zu verarbeiten. Andere (Kevin, David W.) haben gezeigt, wie man das mit Rohren macht:

    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 diesen Code ausprobieren, werden Sie files_foundfeststellen , dass er nicht funktioniert: ist immer "wahr" und der Code gibt immer "keine Dateien gefunden" aus. Grund ist: Jeder Befehl einer Pipeline wird in einer separaten Subshell ausgeführt, sodass die geänderte Variable in der Schleife (separate Subshell) die Variable im Haupt-Shell-Skript nicht ändert. Aus diesem Grund empfehle ich die Verwendung der Prozesssubstitution als "besseres", nützlicheres und allgemeineres Muster.
    Siehe 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:

Michael Brux
quelle
8

(Aktualisiert, um @ Socowis hervorragende Geschwindigkeitsverbesserung einzuschließen)

Mit allen $SHELL, die es unterstützen (dash / zsh / bash ...):

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

Getan.


Ursprüngliche Antwort (kürzer, aber langsamer):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
user569825
quelle
1
Langsam wie Melasse (da für jede Datei eine Shell gestartet wird), aber das funktioniert. +1
Morgengrauen
1
Stattdessen \;können Sie +so viele Dateien wie möglich an eine einzelne übergeben exec. Verwenden Sie dann "$@"innerhalb des Shell-Skripts alle diese Parameter.
Socowi
3
Dieser Code enthält einen Fehler. In der Schleife fehlt das erste Ergebnis. Das liegt daran, dass es $@weggelassen wird, da es normalerweise der Name des Skripts ist. Wir müssen nur hinzufügen dummydazwischen 'und {}so den Ort des Skripts Namen nehmen kann, um sicherzustellen , alle Spiele von der Schleife verarbeitet werden.
BCartolo
Was ist, wenn ich andere Variablen von außerhalb der neu erstellten Shell benötige?
Jodo
OTHERVAR=foo find . -na.....sollte Ihnen den Zugriff $OTHERVARvon dieser neu erstellten Shell aus ermöglichen.
user569825
6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one
bmargulies
quelle
3
for x in $(find ...)wird für jeden Dateinamen mit Leerzeichen unterbrochen. Gleiches gilt für, es find ... | xargssei denn, Sie verwenden -print0und-0
Glenn Jackman
1
Verwenden Sie find . -name "*.txt -exec process_one {} ";"stattdessen. Warum sollten wir xargs verwenden, um Ergebnisse zu sammeln, die wir bereits haben?
Benutzer unbekannt
@userunknown Nun, das hängt alles davon ab, was process_oneist. Wenn es sich um einen Platzhalter für einen tatsächlichen Befehl handelt , stellen Sie sicher, dass dies funktioniert (wenn Sie Tippfehler beheben und anschließend abschließende Anführungszeichen hinzufügen "*.txt). Wenn process_onees sich jedoch um eine benutzerdefinierte Funktion handelt, funktioniert Ihr Code nicht.
Toxalot
@toxalot: Ja, aber es wäre kein Problem, die Funktion in ein aufzurufendes Skript zu schreiben.
Benutzer unbekannt
4

Sie können Ihre findAusgabe im Array speichern, wenn Sie die Ausgabe später wie folgt verwenden möchten:

array=($(find . -name "*.txt"))

Um nun jedes Element in einer neuen Zeile zu drucken, können Sie entweder eine forSchleifeniteration für alle Elemente des Arrays verwenden oder die printf-Anweisung verwenden.

for i in ${array[@]};do echo $i; done

oder

printf '%s\n' "${array[@]}"

Sie können auch verwenden:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Dadurch wird jeder Dateiname in Newline gedruckt

Um die findAusgabe nur in Listenform zu drucken , können Sie eine der folgenden Methoden verwenden:

find . -name "*.txt" -print 2>/dev/null

oder

find . -name "*.txt" -print | grep -v 'Permission denied'

Dadurch werden Fehlermeldungen entfernt und nur der Dateiname als Ausgabe in einer neuen Zeile angegeben.

Wenn Sie etwas mit den Dateinamen tun möchten, ist es gut, sie im Array zu speichern. Andernfalls müssen Sie diesen Speicherplatz nicht belegen, und Sie können die Ausgabe direkt ausdrucken find.

Rakholiya Jenish
quelle
1
Das Durchlaufen des Arrays schlägt mit Leerzeichen in Dateinamen fehl.
EM0
Sie sollten diese Antwort löschen. Es funktioniert nicht mit Leerzeichen in Dateinamen oder Verzeichnisnamen.
JWW
4

Wenn Sie davon ausgehen können, dass die Dateinamen keine Zeilenumbrüche enthalten, können Sie die Ausgabe findmit dem folgenden Befehl in ein Bash-Array einlesen :

readarray -t x < <(find . -name '*.txt')

Hinweis:

  • -tbewirkt readarray, dass Zeilenumbrüche entfernt werden.
  • Es funktioniert nicht, wenn readarrayes sich in einer Pipe befindet, daher die Prozessersetzung.
  • readarray ist seit Bash 4 verfügbar.

Bash 4.4 und höher unterstützt auch den -dParameter zur Angabe des Trennzeichens. Die Verwendung des Nullzeichens anstelle von Zeilenumbruch zur Begrenzung der Dateinamen funktioniert auch in dem seltenen Fall, dass die Dateinamen Zeilenumbrüche enthalten:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarraykann auch wie mapfilemit den gleichen Optionen aufgerufen werden .

Referenz: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

Seppo Enarvi
quelle
Das ist die beste Antwort! Funktioniert mit: * Leerzeichen in Dateinamen * Keine übereinstimmenden Dateien * exitbeim
Durchlaufen
Funktioniert nicht mit allen möglichen Dateinamen, obwohl - dafür sollten Siereadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy
3

Ich verwende gerne find, das zuerst der Variablen zugewiesen und IFS wie folgt auf eine neue Zeile umgeschaltet wird:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Nur für den Fall, dass Sie weitere Aktionen für denselben DATENSatz wiederholen möchten und feststellen möchten, dass die Suche auf Ihrem Server sehr langsam ist (hohe E / A-Auslastung).

Paco
quelle
2

Sie können die von zurückgegebenen Dateinamen findin ein Array wie folgt einfügen:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Jetzt können Sie einfach das Array durchlaufen, um auf einzelne Elemente zuzugreifen und mit ihnen zu tun, was Sie wollen.

Hinweis: Es ist ein Leerraum sicher.

Jahid
quelle
1
Mit Bash 4.4 oder höher können Sie einen einzelnen Befehl anstelle einer Schleife verwenden : mapfile -t -d '' array < <(find ...). Einstellung IFSist nicht erforderlich für mapfile.
Socowi
1

basierend auf anderen Antworten und Kommentaren von @phk, mit fd # 3:
(was immer noch erlaubt, stdin innerhalb der Schleife zu verwenden)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
Florian
quelle
-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Dadurch werden die Dateien aufgelistet und Details zu Attributen angegeben.

Chetangb
quelle
-5

Wie wäre es, wenn Sie grep anstelle von find verwenden?

ls | grep .txt$ > out.txt

Jetzt können Sie diese Datei lesen und die Dateinamen liegen in Form einer Liste vor.

Dhruv Raj Singh Rathore
quelle
6
Nein, tu das nicht. Warum sollte man nicht die Ausgabe von ls analysieren . Das ist zerbrechlich, sehr zerbrechlich.
Fedorqui 'SO hör auf,'