Ich möchte ein /bin/sh
Shell-Skript schreiben, das alle Dateien verarbeitet, die mit einem Platzhalter übereinstimmen. Es ist einfach, 1 oder mehr übereinstimmende Dateien zu verarbeiten. Ich finde es jedoch umständlich, den Fall von 0 übereinstimmenden Dateien zu verarbeiten.
Die offensichtliche Konstruktion ist:
#!/bin/sh
for f in *.ext; do
handle "$f"
done
wobei *.ext
ein oder mehrere Ausdrücke sein können, die die Shell mit Dateipfaden vergleicht, und handle
ein Shell-Befehl ist, der korrekt ausgeführt wird, wenn ein Pfad zu einer vorhandenen Datei angegeben wird, aber fehlschlägt, wenn ein Pfad angegeben wird, der keiner Datei zugeordnet werden kann. (In dem Fall, der diese Frage aufwirft, sind es *.flac
und ffmpeg
, aber ich denke, das spielt keine Rolle.)
Wenn es passende Dateien gibt foo.ext
, bar.ext
dann führt dieses Skript aus
handle "foo.ext"
handle "bar.ext"
wie erwartet. Wenn es jedochnichtalle passenden Dateien, dieses Skript gibt eine Fehlermeldung wie,
handle: *.ext: No such file or directory
Ich glaube, ich verstehe, warum das passiert: dieSchlagmanpage(was meiner Meinung nach auch für gilt /bin/sh
) besagt, dass die „Liste der folgenden Wörter in
erweitert wird, wodurch eine Liste mit Elementen generiert wird. … Wenn die Erweiterung der folgenden Elemente in eine leere Liste resultiert, werden keine Befehle ausgeführt und der Rückgabestatus ist 0.“ Wenn *.ext
„erweitert wird“, enthält die Ergebnisliste anscheinend *.ext
, statt einer leeren Liste. Aber das erklärt nicht, wie man das verhindern kann.
(Update: Es gibt einesh (1)
Manpage vom Heirloom Projectwas die Bourne Shell beschreibt sh
, nicht die spätere Bourne Again Shell bash
. UnterDateinamengenerierung, es heißt dort eindeutig: „Wenn kein Dateiname gefunden wird, der dem Muster entspricht, bleibt das Wort unverändert.“ Es erklärt, warum ein Muster in der Liste sh
verbleibt .)*.ext
Wie kann man diese Schleife kompakt und idiomatisch schreiben, sodass sie nullmal ohne Fehler ausgeführt wird, wenn keine übereinstimmenden Dateien vorhanden sind, aber einmal für jede übereinstimmende Datei? Oder noch besser: Was funktioniert mit mehreren Mustern, wie:
for f in *.ext1 special.ext1 *.ext2; do ...
Aus Portabilitätsgründen bevorzuge ich eine mit kompatible Syntax /bin/sh
. Ich verwende zufällig Mac OS X 10.11.6, hoffe aber auf eine Syntax, die auf jedem Unix-ähnlichen Betriebssystem funktioniert.
Mir sind ein paar unbeholfene, nicht idiomatische Möglichkeiten eingefallen, eine solche Schleife zu schreiben. Wenn ich nicht sofort gute Antworten bekomme, werde ich diese der Vollständigkeit halber als Antworten beisteuern.
Antwort1
Der einfachste portable Weg hierfür besteht darin, die Schleife zu überspringen, wenn die Erweiterung nichts erzeugt, was tatsächlich existiert:
for f in *.ext1 *.ext2; do
[ -e "$f" ] || continue
handle "$f"
done
Erklärung: Angenommen, es gibt mehrere .ext2-Dateien, aber keine .ext1-Dateien. In diesem Fall werden die Platzhalter zu erweitert *.ext1
filea.ext2
fileb.ext2
filec.ext2
. Dies [ -e "*.ext1" ]
schlägt fehl und führt aus continue
, wodurch der Rest dieser Iteration der Schleife übersprungen wird. Dann [ -e "filea.ext2" ]
ist usw. erfolgreich und diese Iterationen der Schleife werden normal ausgeführt.
Übrigens können Sie dies ändern, um beispielsweise [ -f "$f" ] || continue
alles zu überspringen, was keine einfache Datei ist (wenn Sie Verzeichnisse usw. überspringen möchten).
Wenn Sie unter Bash arbeiten, können Sie natürlich shopt -s nullglob
zuerst ausführen, damit keine nicht übereinstimmenden Muster in der Liste verbleiben. Und dann shopt -u nullglob
danach, um unerwartete Nebenwirkungen zu vermeiden (z. B. grep pattern *.nonexistent
wird versucht, von stdin zu lesen, wenn nullglob
festgelegt ist).
Antwort2
Wechseln Sie von der Bourne Shell () zur Bourne Again Shell /bin/bash
( /bin/sh
), und eine einfache Lösung wird möglich.
Derbash(1)
manpageerwähnt dienullglobMöglichkeit:
Wenn gesetzt, erlaubt bash Muster, die mit keiner Datei übereinstimmen (siehePfadnamenerweiterungoben) so erweitert werden, dass eine Nullzeichenfolge und nicht die Erweiterung selbst entsteht.
DerPfadnamenerweiterungAbschnitt sagt:
Nach der Worttrennung durchsucht …bash jedes Wort nach den Zeichen*,?, Und[. Wenn eines dieser Zeichen vorkommt, wird das Wort als Muster betrachtet und durch eine alphabetisch sortierte Liste von Dateinamen ersetzt, die dem Muster entsprechen. Wenn keine übereinstimmenden Dateinamen gefunden werden und die Shell-Optionnullglobnicht aktiviert ist, bleibt das Wort unverändert. Wenn dienullglobOption aktiviert ist und keine Übereinstimmungen gefunden werden, wird das Wort entfernt.…
Wenn Sie also dienullglobOption gibt das gewünschte Verhalten für Muster an. Wenn keine Datei dem Muster entspricht, wird die Schleife nicht ausgeführt.
#!/bin/bash
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
Aber dienullglobOption kann das gewünschte Verhalten für andere Befehle beeinträchtigen. Daher ist es wahrscheinlich sinnvoll, die vorhandene Einstellung vonnullglobund danach wiederherstellen. Glücklicherweise gibt der shopt -p
eingebaute Befehl die Ausgabe in einer Form aus, die als Eingabe wiederverwendet werden kann. Aber er gibt die Ausgabe für alle Optionen aus, also verwenden Sie , grep
um dienullobjEinstellung. Siehe das Protokoll unten (wobei $
die Bash-Eingabeaufforderung angezeigt wird):
$ shopt -p | grep nullglob
shopt -u nullglob
$ shopt -s nullglob
$ shopt -p | grep nullglob
shopt -s nullglob
Die endgültige Bash-Syntax zur Verarbeitung von null oder mehr Dateien, die einem Platzhaltermuster entsprechen, sieht also folgendermaßen aus:
#!/bin/bash
SAVED_NULLGLOB=$(shopt -p | grep nullglob)
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
eval "$SAVED_NULLGLOB"
Außerdem wurden in der Frage mehrere Muster erwähnt, wie:
for f in *.ext1 special.ext1 *.ext2; do ...
DernullglobOption hat keinen Einfluss auf ein Wort in der Liste, das kein „Muster“ ist, und special.ext1
wird daher trotzdem an die Schleife übergeben. Die einzige Lösung hierfür ist@Gordon Davisson's continue
Ausdruck, [ -e "$f" ] || continue
.
Anerkennung: beide@Ignacio Vazquez-AbramsUnd@Gordon Davissonangedeutet bash(1)
und seinenullglobOption. Danke. Da sie nicht sofort eine Antwort mit einem vollständig ausgearbeiteten Beispiel beigesteuert haben, mache ich das selbst.