SH-Syntax für null Dateien, die mit einem Platzhalter übereinstimmen, sowie weitere?

460
Jim DeLaHunt

Ich möchte ein /bin/shShell-Skript schreiben, das alle Dateien behandelt, die mit einem Platzhalterzeichen übereinstimmen. Es ist einfach, eine oder mehrere passende Dateien zu bearbeiten. Ich finde es jedoch umständlich, mit 0 übereinstimmenden Dateien umzugehen.

Das offensichtliche Konstrukt ist:

#!/bin/sh for f in *.ext; do handle "$f" done 

Dabei kann es *.extsich um einen oder mehrere Ausdrücke handeln, die die Shell mit Dateipfaden vergleicht, und es handleist ein Shell-Befehl, der korrekt ausgeführt wird, wenn ein Pfad zu einer vorhandenen Datei angegeben wird. Er schlägt jedoch fehl, wenn ein Pfad angegeben wird, der keiner Datei zugeordnet ist. (Für den Fall, dass diese Frage provoziert wird, sind sie *.flacund ffmpeg, aber ich denke, das spielt keine Rolle.)

Wenn passende Dateien vorhanden foo.extsind bar.ext, wird dieses Skript ausgeführt

handle "foo.ext" handle "bar.ext" 

wie erwartet. Wenn jedoch keine passenden Dateien vorhanden sind, gibt dieses Skript eine Fehlermeldung aus, z. B.

handle: *.ext: No such file or directory 

Ich glaube, ich verstehe, warum das so ist: Die bash- Manpage (von der ich annehme, dass sie /bin/shauch gültig ist ) sagt, dass die "Liste der folgenden Wörter inerweitert ist und eine Liste von Elementen erzeugt. Wenn die Erweiterung der folgenden Elemente zu einem leeren Ergebnis führt werden keine Befehle ausgeführt und der Rückgabestatus ist 0. " Wenn *.ext"erweitert" wird, enthält die Ergebnisliste anscheinend *.exteine leere Liste. Aber das erklärt nicht, wie man es aufhalten kann.

(Update: Es gibt eine sh (1)Manpage aus dem Heirloom-Projekt, die die Bourne-Shell beschreibt sh, nicht die spätere Bourne Again-Shell bash. Bei der Dateinamengenerierung heißt es eindeutig: "Wenn kein Dateiname gefunden wird, der mit dem Muster übereinstimmt, bleibt das Wort zurück unverändert. "Es erklärt, warum shein *.extMuster in der Liste bleibt .)

Was ist eine kompakte, idiomatische Art, diese Schleife zu schreiben, so dass sie null Mal ohne Fehler ausgeführt wird, wenn keine passenden Dateien vorhanden sind, aber einmal für jede übereinstimmende Datei ausgeführt wird? Besser noch, was funktioniert mit mehreren Mustern wie:

for f in *.ext1 special.ext1 *.ext2; do ... 

Ich bevorzuge die Verwendung von kompatibler Syntax /bin/shfür die Portabilität. Ich verwende zufällig Mac OS X 10.11.6, aber ich hoffe auf eine Syntax, die auf jedem Unix-ähnlichen Betriebssystem funktioniert.

Ich habe mir ein paar unbeholfene, nicht idiomatische Möglichkeiten ausgedacht, eine solche Schleife zu schreiben. Wenn ich nicht sofort gute Antworten bekomme, gebe ich diese als Antworten ein.

0
Das ist trivial, wenn Sie bereit sind, auf Bash zu wechseln. Ignacio Vazquez-Abrams vor 6 Jahren 0
@ IgnacioVazquez-Abrams Ich bleibe lieber bei `/ bin / sh`, aber das ist nicht absolut. Ich denke, eine Antwort der Form "Wechseln zu Bash, dann mach das" ist hilfreich. Es wird nicht so gut sein wie eine, die sagt: "So geht's in sh", aber vielleicht gibt es eine solche Antwort nicht. Jim DeLaHunt vor 6 Jahren 0
bash hat `nullglob`, wodurch verhindert wird, dass die Schleife überhaupt läuft. Ignacio Vazquez-Abrams vor 6 Jahren 0
Großartig! Machen Sie daraus eine gute Antwort, und Sie erhalten Punkte. Jim DeLaHunt vor 6 Jahren 0

2 Antworten auf die Frage

2
Gordon Davisson

Der einfachste tragbare Weg, dies zu tun, besteht darin, die Schleife zu überspringen, falls die Erweiterung nicht etwas produziert, was tatsächlich existiert:

for f in *.ext1 *.ext2; do [ -e "$f" ] || continue handle "$f" done 

Erläuterung: Angenommen, es gibt mehrere .ext2-Dateien, jedoch keine .ext1-Dateien. In diesem Fall erweitern sich die Platzhalter auf *.ext1 filea.ext2 fileb.ext2 filec.ext2. Also [ -e "*.ext1" ]scheitert und es läuft continue, was den Rest der Iteration der Schleife überspringt. Dann [ -e "filea.ext2" ]gelingt usw. und die Iterationen der Schleife laufen normal.

Übrigens, Sie können dies ändern, um z. B. [ -f "$f" ] || continuealles zu überspringen, das keine einfache Datei ist (wenn Sie Verzeichnisse usw. überspringen möchten).

Wenn Sie unter bash laufen, können shopt -s nullglobSie natürlich zuerst laufen, so dass keine unübertroffenen Muster in der Liste verbleiben. Und dann shopt -u nullglobdanach, um unerwartete Nebenwirkungen zu vermeiden (z. B. grep pattern *.nonexistentwird versucht, von stdin zu lesen, wenn nullglobgesetzt).

Cleverer Gebrauch von "weiter" und exzellente Erklärung. Ich denke, die Verwendung von `nullglob` wird zu einer zweiten Antwort, wenn ich es richtig verstehe. @Ignacio Vazquez-Abrahams erwähnte es in einem Kommentar unter der Frage. Jim DeLaHunt vor 6 Jahren 0
0
Jim DeLaHunt

Wechseln Sie von der Bourne-Shell ( /bin/bash) zur Bourne-Shell ( /bin/sh), und eine einfache Lösung wird möglich.

Die bash(1)Manpage erwähnt die Nullglob- Option:

Wenn gesetzt, erlaubt bash Mustern, die mit keiner Datei übereinstimmen (siehe Pfadnamenerweiterung oben), zu einer Nullzeichenfolge zu expandieren.

Der Abschnitt zur Erweiterung des Pfadnamens sagt:

Nach dem Wort Spaltung scannt ... bash jedes Wort für die Zeichen *, ? und [ . Wenn eines dieser Zeichen erscheint, 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-Option nullglob nicht aktiviert ist, bleibt das Wort unverändert. Wenn die Option nullglob gesetzt ist und keine Übereinstimmungen gefunden werden, wird das Wort entfernt.

Wenn Sie also die Nullglob- Option festlegen, wird das gewünschte Verhalten für Muster festgelegt. Wenn keine Datei mit dem Muster übereinstimmt, wird die Schleife nicht ausgeführt.

#!/bin/bash shopt -s nullglob for f in *.ext; do handle "$f" done 

Die Option nullglob kann jedoch das gewünschte Verhalten für andere Befehle beeinträchtigen. Daher ist es wahrscheinlich ratsam, die vorhandene Einstellung von nullglob zu speichern und anschließend wiederherzustellen. Glücklicherweise gibt der shopt -pintegrierte Befehl die Ausgabe in einer Form aus, die als Eingabe wiederverwendet werden kann. Es gibt jedoch eine Ausgabe für alle Optionen grepaus. Verwenden Sie also die Einstellung nullobj . Siehe das Protokoll unten (wobei $die Eingabeaufforderung für die Bash-Anweisung steht):

$ shopt -p | grep nullglob shopt -u nullglob $ shopt -s nullglob $ shopt -p | grep nullglob shopt -s nullglob 

Die abschließende Bash-Syntax, mit der null oder mehr Dateien behandelt werden, 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" 

In der Frage wurden auch mehrere Muster genannt:

for f in *.ext1 special.ext1 *.ext2; do ... 

Die Option nullglob wirkt sich nicht auf ein Wort in der Liste aus, bei dem es sich nicht um ein "Muster" handelt. Es wird also special.ext1weiterhin an die Schleife übergeben. Die einzige Lösung für dieses ist @Gordon Davisson ‚s continueAusdruck [ -e "$f" ] || continue.

Danksagung: Sowohl @Ignacio Vazquez-Abrams als auch @Gordon Davisson spielten auf bash(1)seine Nullglob- Option an. Vielen Dank. Da sie nicht sofort mit einem ausführlichen Beispiel zur Antwort beigetragen haben, tue ich das selbst.