Führen Sie den Befehl für alle Dateien in einem Verzeichnis aus

290

Könnte jemand bitte den Code bereitstellen, um Folgendes zu tun: Angenommen, es gibt ein Verzeichnis von Dateien, die alle über ein Programm ausgeführt werden müssen. Das Programm gibt die Ergebnisse als Standard aus. Ich benötige ein Skript, das in ein Verzeichnis geht, den Befehl für jede Datei ausführt und die Ausgabe in einer großen Ausgabedatei zusammenfasst.

So führen Sie den Befehl beispielsweise für eine Datei aus:

$ cmd [option] [filename] > results.out
Der Maestro
quelle
3
Ich möchte die Frage ergänzen. Kann man das mit xargs machen? zB ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray
2
Es kann, aber Sie möchtenls wahrscheinlich nicht zum Fahren verwenden xargs. Wenn cmdes überhaupt kompetent geschrieben ist, können Sie es vielleicht einfach tun cmd <wildcard>.
Tripleee

Antworten:

425

Der folgende Bash-Code übergibt $ file an den Befehl, wobei $ file jede Datei in / dir darstellt

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

Beispiel

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt
Andrew Logvinov
quelle
23
Wenn keine Dateien vorhanden sind /dir/, wird die Schleife immer noch einmal mit dem Wert '*' für ausgeführt $file, was möglicherweise unerwünscht ist. Um dies zu vermeiden, aktivieren Sie nullglob für die Dauer der Schleife. Fügen Sie diese Zeile vor der Schleife shopt -s nullglobund diese Zeile nach der Schleife hinzu shopt -u nullglob #revert nullglob back to it's normal default state.
Stew-au
43
+1, und es hat mich nur meine ganze Tapetensammlung gekostet. Alle nach mir verwenden doppelte Anführungszeichen. "$ file"
Behrooz
Wenn die Ausgabedatei innerhalb der Schleife identisch ist, ist es viel effizienter, außerhalb der Schleife umzuleiten done >results.out(und wahrscheinlich können Sie sie überschreiben, anstatt sie anzuhängen, wie ich hier angenommen habe).
Tripleee
Wie erhalten Sie einzelne Ergebnisdateien, die benutzerdefiniert nach ihren Eingabedateien benannt sind?
Timothy Swan
1
Seien Sie vorsichtig, indem Sie diesen Befehl für eine große Anzahl von Dateien im Verzeichnis verwenden. Verwenden Sie stattdessen find -exec.
Kolisko
182

Wie wäre es damit:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1Das Argument verhindert, dass find rekursiv in Unterverzeichnisse absteigt. (Wenn Sie möchten, dass solche verschachtelten Verzeichnisse verarbeitet werden, können Sie dies weglassen.)
  • -type -f Gibt an, dass nur einfache Dateien verarbeitet werden.
  • -exec cmd option {}Weist es an, cmdmit der optionfür jede gefundene Datei angegebenen Datei auszuführen , wobei der Dateiname ersetzt wird{}
  • \; bezeichnet das Ende des Befehls.
  • Schließlich wird die Ausgabe aller einzelnen cmdAusführungen umgeleitet results.out

Wenn Sie sich jedoch für die Reihenfolge interessieren, in der die Dateien verarbeitet werden, ist es möglicherweise besser, eine Schleife zu schreiben. Ich denke, findverarbeitet die Dateien in Inode-Reihenfolge (obwohl ich mich darin irren könnte), was möglicherweise nicht das ist, was Sie wollen.

Jim Lewis
quelle
1
Dies ist der richtige Weg, um Dateien zu verarbeiten. Die Verwendung einer for-Schleife ist aus vielen Gründen fehleranfällig. Das Sortieren kann auch mit anderen Befehlen wie statund erfolgen sort, was natürlich von den Sortierkriterien abhängt.
Tuxdna
1
Wenn ich zwei Befehle ausführen möchte, wie würde ich sie nach der -execOption verknüpfen ? Muss ich sie in einfache Anführungszeichen setzen oder so?
Frei
findist immer die beste Option, da Sie mit der Option nach Dateinamenmuster filtern -nameund dies in einem einzigen Befehl tun können.
João Pimentel Ferreira
3
@frei die Antwort auf Ihre Frage ist hier: stackoverflow.com/a/6043896/1243247 aber im Grunde nur -execOptionen hinzufügen :find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira
2
Wie können Sie den Dateinamen als Option referenzieren?
Toskan
54

Ich mache das auf meinem Himbeer-Pi von der Kommandozeile aus, indem ich Folgendes ausführe:

for i in *;do omxplayer "$i";done
Robgraves
quelle
7

Die akzeptierten / hoch bewerteten Antworten sind großartig, aber es fehlen ein paar Details. Dieser Beitrag behandelt die Fälle, wie Sie besser damit umgehen können, wenn die Erweiterung des Shell-Pfadnamens (Glob) fehlschlägt, wenn Dateinamen eingebettete Zeilenumbrüche / Strichsymbole enthalten und die Umleitung der Befehlsausgabe aus der for-Schleife verschoben wird, wenn die Ergebnisse in a geschrieben werden Datei.

Wenn Sie die Shell-Glob-Erweiterung mit ausführen, besteht die *Möglichkeit, dass die Erweiterung fehlschlägt, wenn keine Dateien im Verzeichnis vorhanden sind und eine nicht erweiterte Glob-Zeichenfolge an den auszuführenden Befehl übergeben wird, was zu unerwünschten Ergebnissen führen kann. Die bashShell bietet hierfür eine erweiterte Shell-Option nullglob. Die Schleife wird also im Grunde genommen wie folgt in dem Verzeichnis, das Ihre Dateien enthält

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Auf diese Weise können Sie die for-Schleife sicher verlassen, wenn der Ausdruck ./*keine Dateien zurückgibt (wenn das Verzeichnis leer ist).

oder auf POSIX-konforme Weise ( nullglobist bashspezifisch)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Auf diese Weise können Sie in die Schleife gehen, wenn der Ausdruck einmal fehlschlägt, und die Bedingung [ -f "$file" ]überprüfen, ob die nicht erweiterte Zeichenfolge ./*ein gültiger Dateiname in diesem Verzeichnis ist, was nicht der Fall wäre. Wenn diese Bedingung fehlschlägt, kehren continuewir zu der forSchleife zurück, die anschließend nicht ausgeführt wird.

Beachten Sie auch die Verwendung von --kurz vor dem Übergeben des Dateinamenarguments. Dies ist erforderlich, da die Shell-Dateinamen, wie bereits erwähnt, an beliebiger Stelle im Dateinamen Bindestriche enthalten können. Einige der Shell-Befehle interpretieren dies und behandeln sie als Befehlsoption, wenn der Name nicht richtig in Anführungszeichen gesetzt wird, und führen den Befehl aus, wenn das Flag bereitgestellt wird.

Das --signalisiert in diesem Fall das Ende der Befehlszeilenoptionen. Dies bedeutet, dass der Befehl keine Zeichenfolgen über diesen Punkt hinaus als Befehlsflags, sondern nur als Dateinamen analysieren sollte.


Das doppelte Zitieren der Dateinamen löst die Fälle, in denen die Namen Glob-Zeichen oder Leerzeichen enthalten. * Nix-Dateinamen können aber auch Zeilenumbrüche enthalten. Daher beschränken wir Dateinamen mit dem einzigen Zeichen, das nicht Teil eines gültigen Dateinamens sein kann - dem Null-Byte ( \0). Da bashintern Stilzeichenfolgen Cverwendet werden, in denen die Nullbytes verwendet werden, um das Ende der Zeichenfolge anzugeben, ist dies der richtige Kandidat dafür.

So mit der printfdie Möglichkeit , Shell abgrenzen Dateien mit diesem NULL - Byte mit -dOption readBefehl können wir tun unten

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

Die nullglobund die printfwerden umbrochen, (..)was bedeutet, dass sie im Grunde genommen in einer Unter-Shell (untergeordnete Shell) ausgeführt werden, da nach dem nullglobBeenden des Befehls die Option vermieden wird, über die übergeordnete Shell nachzudenken. Die -d ''Befehlsoption readist nicht POSIX-kompatibel und benötigt daher eine bashShell, um dies zu tun. Mit dem findBefehl kann dies wie folgt erfolgen

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

Für findImplementierungen, die nicht unterstützt werden -print0(außer den GNU- und FreeBSD-Implementierungen), kann dies mit emuliert werdenprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Ein weiterer wichtiger Fix besteht darin, die Neuausrichtung aus der for-Schleife zu verschieben, um eine hohe Anzahl von Datei-E / A zu reduzieren. Bei Verwendung innerhalb der Schleife muss die Shell Systemaufrufe zweimal für jede Iteration der for-Schleife ausführen, einmal zum Öffnen und einmal zum Schließen des der Datei zugeordneten Dateideskriptors. Dies wird zu einem Engpass für Ihre Leistung beim Ausführen großer Iterationen. Empfohlener Vorschlag wäre, es außerhalb der Schleife zu verschieben.

Sie können den obigen Code mit diesen Korrekturen erweitern

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

Dadurch wird der Inhalt Ihres Befehls für jede Iteration Ihrer Dateieingabe auf stdout gesetzt. Wenn die Schleife endet, öffnen Sie die Zieldatei einmal, um den Inhalt des stdout zu schreiben und zu speichern. Die äquivalente findVersion davon wäre

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out
Inian
quelle
1
+1, um zu überprüfen, ob die Datei vorhanden ist. Bei der Suche in einem nicht vorhandenen Verzeichnis enthält $ file die Regex-Zeichenfolge "/ invald_dir / *", die keinen gültigen Dateinamen enthält.
cdalxndr
3

Ein schneller und schmutziger Weg, der die Arbeit manchmal erledigt, ist:

find directory/ | xargs  Command 

Um beispielsweise die Anzahl der Zeilen in allen Dateien im aktuellen Verzeichnis zu ermitteln, haben Sie folgende Möglichkeiten:

find . | xargs wc -l
Rahul
quelle
8
@ Hubert Warum hast du Zeilenumbrüche in deinen Dateinamen?!
musicin3d
2
Es geht nicht um das "Warum", sondern um die Richtigkeit - Dateinamen müssen keine druckbaren Zeichen enthalten, sie müssen nicht einmal gültige UTF-8-Sequenzen sein. Was eine Newline ist, hängt stark von der Codierung ab. Eine Codierung ♀ ist die Newline einer anderen. Siehe Codepage 437
Hubert Kario
2
Komm schon, wirklich? Dies funktioniert in 99,9% der Fälle und er sagte "schnell und schmutzig"
Edoardo
Ich bin kein Fan von "schnellen und schmutzigen" (AKA "kaputten") Bash-Skripten. Früher oder später endet es mit Dingen wie "Moved ~/.local/share/steam. Ran Steam. Es löschte alles auf dem System, das dem Benutzer gehört." Fehlerbericht.
Reduzierung der Aktivität
Dies funktioniert auch nicht mit Dateien, deren Name Leerzeichen enthält.
Shamas S - Wiedereinsetzung Monica
2

Ich musste alle .md-Dateien von einem Verzeichnis in ein anderes kopieren, also habe ich Folgendes getan.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

Was ziemlich schwer zu lesen ist, also lasst es uns zusammenfassen.

erste CD in das Verzeichnis mit Ihren Dateien,

for i in **/*.md; für jede Datei in Ihrem Muster

mkdir -p ../docs/"$i"Erstellen Sie dieses Verzeichnis in einem Dokumentordner außerhalb des Ordners, der Ihre Dateien enthält. Dadurch wird ein zusätzlicher Ordner mit demselben Namen wie diese Datei erstellt.

rm -r ../docs/"$i" Entfernen Sie den zusätzlichen Ordner, der als Ergebnis von erstellt wurde mkdir -p

cp "$i" "../docs/$i" Kopieren Sie die eigentliche Datei

echo "$i -> ../docs/$i" Echo was du getan hast

; done Lebe glücklich bis ans Ende

Eric Wooley
quelle
Hinweis: Damit dies **funktioniert, muss die globstarShell-Option festgelegt werden:shopt -s globstar
Hubert Kario
2

Sie können verwenden xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 bewirkt, dass jeweils 1 Element übergeben wird

-d '\n'make output of lswird basierend auf der neuen Zeile aufgeteilt.

Al Mamun
quelle
1

Basierend auf dem Ansatz von @Jim Lewis:

Hier ist eine schnelle Lösung zum Verwenden findund Sortieren von Dateien nach ihrem Änderungsdatum:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Zum Sortieren siehe:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time

tuxdna
quelle
Dies wird nicht funktionieren, wenn die Dateien Zeilenumbrüche in ihren Namen haben
Hubert Kario
1
@HubertKario Vielleicht möchten Sie mehr darüber lesen, -print0für findund -0für xargswelche Nullzeichen anstelle von Leerzeichen (einschließlich Zeilenumbrüchen) verwendet werden.
Tuxdna
Ja, die Verwendung -print0ist etwas, das hilft, aber die gesamte Pipeline muss so etwas verwenden und sortist es auch nicht
Hubert Kario
1

Ich denke, die einfache Lösung ist:

sh /dir/* > ./result.txt
yovie
quelle
2
Hast du die Frage richtig verstanden? Dadurch wird lediglich versucht, jede Datei im Verzeichnis über die Shell auszuführen - als wäre es ein Skript.
rdas
1

Maximale Tiefe

Ich fand, dass es gut mit Jim Lewis 'Antwort funktioniert. Fügen Sie einfach ein bisschen wie folgt hinzu:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Sortierung

Wenn Sie in Sortierreihenfolge ausführen möchten, ändern Sie es wie folgt:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Dies wird nur als Beispiel in der folgenden Reihenfolge ausgeführt:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Unbegrenzte Tiefe

Wenn Sie unter bestimmten Bedingungen in unbegrenzter Tiefe ausführen möchten, können Sie Folgendes verwenden:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

Fügen Sie dann wie folgt über jede Datei in den untergeordneten Verzeichnissen ein:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

und irgendwo im Hauptteil der übergeordneten Datei:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
Chetabahana
quelle