Sie haben nun viele Befehle in Ihrem Werkzeugkasten und ein umfassendes Verständnis von der Shell. Jetzt ist es an der Zeit zu lernen… wie Sie Befehle ausführen. Moment mal, haben Sie nicht schon seit Beginn des Buchs Befehle ausgeführt? Im Prinzip ja, aber nur auf zwei Arten. Die erste ist die normale Ausführung eines einfachen Befehls:
$ grep Nutshell animals.txt
Die zweite Art ist eine Pipeline aus einfachen Befehlen, wie in Kapitel 1 behandelt:
$ cut -f1 grades | sort | uniq -c | sort -nr
In diesem Kapitel zeige ich Ihnen elf weitere Möglichkeiten, einen Befehl auszuführen, und erläutere, weshalb Sie diese unbedingt lernen sollten. Jede Technik hat ihre Pros und Kontras, und je mehr Techniken Sie kennen, umso flexibler und effizienter können Sie mit Linux umgehen. Ich konzentriere mich für den Augenblick auf die Grundlagen der einzelnen Techniken. In den nächsten beiden Kapiteln lernen Sie dann raffiniertere Beispiele kennen.
Eine Liste ist eine Abfolge von Befehlen auf einer einzigen Kommandozeile. Sie haben bereits eine Art von Liste kennengelernt – eine Pipeline –, doch die Shell unterstützt auch noch weitere mit anderen Verhaltensweisen:
Bedingte Listen
Jeder Befehl hängt vom Erfolg oder Scheitern eines vorangegangenen Befehls ab.
Bedingungslose Listen
Befehle werden einfach nacheinander ausgeführt.
Nehmen wir einmal an, Sie wollten eine Datei new.txt in einem Verzeichnis dir anlegen. Eine typische Abfolge von Befehlen könnte so aussehen:
$ cd dir Betreten des Verzeichnisses
$ touch new.txt Anlegen der Datei
Erkennen Sie, dass der zweite Befehl vom Erfolg des ersten abhängt? Wenn das Verzeichnis dir nicht existiert, ist es sinnlos, den Befehl touch auszuführen. Die Shell erlaubt es Ihnen, diese Abhängigkeit explizit zu formulieren. Wenn Sie den Operator && (ausgesprochen »und«) zwischen die zwei Befehle setzen, die auf einer einzigen Zeile stehen:
$ cd dir && touch new.txt
wird der zweite Befehl (touch) nur ausgeführt, wenn der erste Befehl (cd) erfolgreich war. Das gezeigte Beispiel ist eine bedingte Liste aus zwei Befehlen. (Um zu erfahren, was es für einen Befehl bedeutet, »erfolgreich zu sein«, siehe »Exit-Codes zeigen Erfolg oder Scheitern an« auf Seite 131.)
Wahrscheinlich führen Sie jeden Tag Befehle aus, die von früheren Befehlen abhängen. Haben Sie zum Beispiel irgendwann einmal eine Sicherungskopie (Backup-Kopie) einer Datei erstellt, das Original verändert und dann die Sicherungskopie gelöscht, als Sie fertig waren?
$ cp myfile.txt myfile.safe Herstellen einer Backup-Kopie
$ nano myfile.txt Ändern des Originals
$ rm myfile.safe Löschen des Backups
Jeder dieser Befehle ist nur dann sinnvoll, wenn der vorhergehende Befehl erfolgreich war. Deshalb ist diese Sequenz ein Kandidat für eine bedingte Liste:
$ cp myfile.txt myfile.safe && nano myfile.txt && rm myfile.safe
Ein anderes Beispiel. Falls Sie das Versionskontrollsystem Git zum Pflegen Ihrer Dateien verwenden, sind Sie vermutlich mit der folgenden Sequenz aus Befehlen vertraut, die Sie ausführen, nachdem Sie einige Dateien geändert haben: Sie führen git add aus, um Dateien für einen Commit vorzubereiten, dann kommt git commit und schließlich git push, um Ihre bestätigten Änderungen zu teilen. Würde einer dieser Befehle fehlschlagen, dann würden Sie den Rest nicht ausführen (weil Sie erst die Ursache des Fehlers beseitigen müssten). Daher eignen sich diese drei Befehle ebenfalls gut für eine bedingte Liste:
$ git add . && git commit -m"fixed a bug" && git push
Genau wie der &&-Operator einen zweiten Befehl nur ausführt, wenn der erste erfolgreich war, führt der damit verwandte Operator || (ausgesprochen »oder«) einen zweiten Befehl nur dann aus, wenn der erste fehlschlägt. Zum Beispiel versucht der folgende Befehl, dir zu betreten. Sollte das nicht gelingen, wird dir erzeugt:1
Sie finden den ||-Operator üblicherweise in Skripten, wo er dafür sorgt, dass das Skript beendet wird, wenn ein Fehler auftritt:
# Falls ein Verzeichnis nicht betreten werden kann,
# beende das Ganze mit dem Fehlercode 1.
cd dir || exit 1
Kombinieren Sie die Operatoren && und ||, um kompliziertere Aktionen für Erfolg und Fehlschlagen einzurichten. Der folgende Befehl versucht, das Verzeichnis dir zu betreten. Schlägt dies fehl, wird das Verzeichnis angelegt und dann betreten. Sollte alles scheitern, gibt der Befehl eine Fehlermeldung aus:
$ cd dir || mkdir dir && cd dir || echo "Ich bin gescheitert"
Die Befehle in einer bedingten Liste müssen keine einfachen Befehle sein, es kann sich auch um Pipelines und andere kombinierte Befehle handeln.
Exit-Codes zeigen Erfolg oder Scheitern an
Was bedeutet es für einen Linux-Befehl, erfolgreich zu sein oder zu scheitern? Jeder Linux-Befehl erzeugt ein Ergebnis, wenn er beendet wird, einen sogenannten Exit-Code. Üblicherweise bedeutet der Exit-Code null, dass der Befehl erfolgreich war. Ist der Exit-Code nicht null, war der Befehl nicht erfolgreich.2 Betrachten Sie den Exit-Code des zuletzt abgeschlossenen Befehls der Shell, indem Sie die spezielle Shell-Variable ausgeben, deren Name ein Fragezeichen ist (?):
$ ls myfile.txt
myfile.txt
$ echo $? Ausgeben des Werts der Variablen ?
0 ls war erfolgreich.
$ cp nonexistent.txt somewhere.txt
cp: cannot stat 'nonexistent.txt': No such file or directory
$ echo $?
1 cp ist gescheitert.
Befehle in einer Liste müssen nicht voneinander abhängig sein. Falls Sie die Befehle mit Semikola trennen, werden sie einfach nacheinander ausgeführt. Erfolg oder Scheitern eines Befehls haben keinen Einfluss auf nachfolgende Befehle in der Liste.
Ich nutze bedingungslose Listen gern, um ein paar Befehle auszuführen, nachdem ich mit meiner Arbeit für den Tag fertig bin. Hier ist einer, der zwei Stunden (7.200 Sekunden) schläft (nichts tut) und dann meine wichtigen Dateien in einem Backup sichert:
$ sleep 7200; cp -a ~/important-files /mnt/backup_drive
Es folgt ein ähnlicher Befehl, der als einfaches Erinnerungssystem funktioniert: Erst schläft er für fünf Minuten, und dann sendet er mir eine E-Mail:3
$ sleep 300; echo "Denk dran, mit dem Hund rauszugehen" | mail -s reminder $USER
Bedingungslose Listen dienen vor allem der Bequemlichkeit: Sie erzielen die gleichen Ergebnisse (meistens), als würde man jeden Befehl einzeln eintippen und hinterher die Enter-Taste drücken. Der einzige signifikante Unterschied betrifft die Exit-Codes. In einer bedingungslosen Liste werden die Exit-Codes der einzelnen Befehle mit Ausnahme des letzten weggeworfen. Nur der Exit-Code des letzten Befehls, der aus der Liste ausgeführt wird, wird der Shell-Variablen ? zugewiesen:
$ mv file1 file2; mv file2 file3; mv file3 file4
$ echo $?
0 Exit-Code für "mv file3 file4"
Substitution bedeutet das automatische Ersetzen des Texts eines Befehls durch anderen Text. Ich zeige Ihnen zwei Typen mit leistungsstarken Möglichkeiten:
Befehlssubstitution
Ein Befehl wird durch seine Ausgabe ersetzt.
Prozesssubstitution
Ein Befehl wird durch eine Datei ersetzt (quasi).
Nehmen wir einmal an, Sie hätten einige Tausend Textdateien, die Musiktitel repräsentieren. Jede Datei enthält einen Songtitel, den Künstlernamen, den Albumtitel und den Text des Songs:
Title: Carry On Wayward Son
Artist: Kansas
Album: Leftoverture
Carry on my wayward son
There'll be peace when you are done
...
Sie möchten die Dateien gern nach Künstlern geordnet in Unterverzeichnissen organisieren. Um diese Aufgabe von Hand auszuführen, könnten Sie mit grep nach allen Lieddateien von Kansas suchen:
$ grep -l "Artist: Kansas" *.txt
carry_on_wayward_son.txt
dust_in_the_wind.txt
belexes.txt
und dann die einzelnen Dateien in ein Verzeichnis kansas verschieben:
$ mkdir kansas
$ mv carry_on_wayward_son.txt kansas
$ mv dust_in_the_wind.txt kansas
$ mv belexes.txt kansas
Lästig, nicht wahr? Es wäre doch großartig, wenn Sie der Shell sagen könnten: »Verschiebe alle Dateien, die den String Artist: Kansas enthalten, in das Verzeichnis kansas«. In Linux-Begriffen würden Sie gern die Liste der Namen aus dem vorherigen grep -l-Befehl nehmen und sie an mv übergeben. Nun, das können Sie ganz leicht mithilfe einer Shell-Eigenschaft namens Befehlssubstitution erledigen:
$ mv $(grep -l "Artist: Kansas" *.txt) kansas
Die Syntax:
$(beliebiger Befehl hier)
führt den Befehl in den runden Klammern aus und ersetzt ihn durch seine Ausgabe. Auf der vorherigen Kommandozeile wird also der Befehl grep -l durch die Liste der Dateinamen ersetzt, die er ausgibt, so als hätten Sie die Dateinamen folgendermaßen eingetippt:
$ mv carry_on_wayward_son.txt dust_in_the_wind.txt belexes.txt kansas
Immer wenn Sie feststellen, dass Sie die Ausgabe eines Befehls auf eine spätere Kommandozeile kopieren, können Sie normalerweise mit einer Befehlssubstitution Zeit sparen. Sie können sogar Aliase bei der Befehlssubstitution verwenden, weil ihr Inhalt in einer Subshell läuft, die Kopien der Aliase ihrer Eltern-Shell enthält.
Hier ist ein weiteres Beispiel. Nehmen wir einmal an, Sie hätten die Kontoauszüge mehrerer Jahre im PDF-Format heruntergeladen. Die heruntergeladenen Dateien tragen Namen, die das Jahr, den Monat und den Tag des Auszugs enthalten, wie etwa eStmt_2021-08-26.pdf für den 26. August 2021.4 Sie würden sich gern den neuesten Auszug im aktuellen Verzeichnis anschauen. Das könnten Sie manuell erledigen: Listen Sie das Verzeichnis auf, suchen Sie die Datei mit dem neuesten Datum (das wird die letzte Datei in der Liste sein) und zeigen Sie sie mit einem Linux-PDF-Viewer wie okular an. Doch warum sollten Sie all das von Hand erledigen? Lassen Sie sich die ganze Arbeit von der Befehlssubstitution abnehmen. Erzeugen Sie einen Befehl, der den Namen der neuesten PDF-Datei im Verzeichnis ausgibt:
$ ls eStmt*pdf | tail -n1
und übergeben Sie diesen mittels Befehlssubstitution an okular:
$ okular $(ls eStmt*pdf | tail -n1)
Der Befehl ls listet alle Kontoauszugsdateien auf, und tail gibt nur die letzte aus, also etwa eStmt_2021-08-26.pdf. Die Befehlssubstitution setzt diesen einen Dateinamen direkt auf die Kommandozeile, so als hätten Sie okular eStmt_2021-08-26.pdf eingetippt.
Hinweis Die Originalsyntax für die Befehlssubstitution verwendete Backquotes (Backticks). Die folgenden beiden Befehle sind äquivalent: |
|
$ echo Heute ist $(date +%A). Heute ist Samstag. $ echo Heute ist `date +%A`. Heute ist Samstag. |
|
Backticks werden von den meisten Shells unterstützt. Die $()-Syntax lässt sich jedoch einfacher verschachteln: |
|
$ echo $(date +%A) | tr a-z A-Z einzeln SAMSTAG echo Heute ist $(echo $(date +%A) | tr a-z A-Z)! geschachtelt Heute ist SAMSTAG! |
In Skripten wird die Befehlssubstitution oft eingesetzt, um die Ausgabe eines Befehls in einer Variablen zu speichern:
Variablenname=$(irgendein Befehl hier)
Um zum Beispiel die Dateinamen zu erhalten, die Songs von Kansas enthalten, und sie in einer Variablen zu speichern, verwenden Sie die Befehlssubstitution folgendermaßen:
$ kansasFiles=$(grep -l "Artist: Kansas" *.txt)
Die Ausgabe könnte mehrere Zeilen haben; um also alle Newline-Zeichen zu behalten, sollten Sie den Wert quotieren, also mit Anführungszeichen schützen, wenn Sie ihn benutzen:
$ echo "$kansasFiles"
Die Befehlssubstitution, die Sie gerade gesehen haben, ersetzt einen Befehl an Ort und Stelle durch seine Ausgabe, also als String. Die Prozesssubstitution ersetzt einen Befehl ebenfalls durch seine Ausgabe, behandelt die Ausgabe aber, als wäre sie in einer Datei gespeichert. Dieser entscheidende Unterschied wirkt auf den ersten Blick verwirrend, deshalb erkläre ich ihn Schritt für Schritt.
Nehmen wir einmal an, Sie befänden sich in einem Verzeichnis mit JPEG-Bilddateien namens 1.jpg bis 1000.jpg, allerdings fehlen einige Dateien mysteriöserweise, und Sie möchten diese identifizieren. Erzeugen Sie ein solches Verzeichnis mit den folgenden Befehlen:
$ mkdir /tmp/jpegs && cd /tmp/jpegs
$ touch {1..1000}.jpg
$ rm 4.jpg 981.jpg
Eine schlechte Methode, um fehlende Dateien zu finden, besteht darin, das Verzeichnis aufzulisten, numerisch zu sortieren und dann durch »scharfes Hinschauen« nach Lücken zu suchen:
$ ls -1 | sort -n | less
1.jpg
2.jpg
3.jpg
5.jpg 4.jpg fehlt
...
Bei einer robusteren, automatisierten Lösung werden die vorhandenen Dateinamen mit dem Befehl diff mit einer kompletten Liste der Namen von 1.jpg bis 1000.jpg verglichen. Dies lässt sich zum Beispiel mit temporären Dateien bewerkstelligen. Speichern Sie die vorhandenen Dateinamen sortiert in einer temporären Datei original-list:
$ ls *.jpg | sort -n > /tmp/original-list
Geben Sie dann eine vollständige Liste der Dateinamen von 1.jpg bis 1000.jpg in eine weitere temporäre Datei namens full-list aus, indem Sie mit seq die Integer-Werte 1 bis 1000 generieren und dann an jede Zeile mit sed ».jpg« anhängen:
$ seq 1 1000 | sed 's/$/.jpg/' > /tmp/full-list
Wenn Sie nun die beiden temporären Dateien mit dem Befehl diff vergleichen, werden Sie entdecken, dass 4.jpg und 981.jpg fehlen. Anschließend löschen Sie die temporären Dateien:
$ diff /tmp/original-list /tmp/full-list
3a4
> 4.jpg
979a981
> 981.jpg
$ rm /tmp/original-list /tmp/full-list hinterher aufräumen
Das sind viele Schritte. Wäre es nicht toll, wenn man die zwei Namenslisten direkt vergleichen könnte und sich nicht mit temporären Dateien herumschlagen müsste? Die Herausforderung liegt darin, dass diff nicht zwei Listen von der Standardeingabe vergleichen kann; es braucht Dateien als Argumente.5 Eine Lösung für dieses Problem bietet die Prozesssubstitution. Sie lässt beide Listen für diff als Dateien erscheinen. (Der Kasten »Wie die Prozesssubstitution funktioniert« auf Seite 137 liefert die technischen Einzelheiten.) Die Syntax:
<(irgendein Befehl hier)
führt den Befehl in einer Subshell aus und stellt dessen Ausgabe so dar, als wäre sie in einer Datei enthalten. Zum Beispiel repräsentiert der folgende Ausdruck die Ausgabe von ls -1 | sort -n, als würde sie in einer Datei stehen:
<(ls -1 | sort -n)
Sie können die Datei mit cat verketten:
$ cat <(ls -1 | sort -n)
1.jpg
2.jpg
...
Und Sie können die Datei mit cp kopieren:
$ cp <(ls -1 | sort -n) /tmp/listing
$ cat /tmp/listing
1.jpg
2.jpg
...
Wie Sie nun sehen, können Sie die Datei mithilfe von diff mit einer anderen Datei vergleichen. Beginnen Sie mit den zwei Befehlen, die Ihre zwei temporären Dateien generiert haben:
ls *.jpg | sort -n
seq 1 1000 | sed 's/$/.jpg/'
Wenden Sie eine Prozesssubstitution an, damit diff sie als Dateien behandeln kann, und Sie erhalten die gleiche Ausgabe wie zuvor, allerdings ohne die Verwendung temporärer Dateien:
$ diff <(ls *.jpg | sort -n) <(seq 1 1000 | sed 's/$/.jpg/')
3a4
> 4.jpg
979a981
> 981.jpg
Säubern Sie die Ausgabe, indem Sie mit grep die Zeilen herausfiltern, die mit > beginnen, und die ersten zwei Zeichen mit cut wegschneiden. Schon wissen Sie über die fehlenden Dateien Bescheid:
$ diff <(ls *.jpg | sort -n) <(seq 1 1000 | sed 's/$/.jpg/') \
| grep '>' \
| cut -c3-
4.jpg
981.jpg
Die Prozesssubstitution hat die Art und Weise verändert, wie ich die Kommandozeile einsetze. Befehle, die nur aus Dateien lesen konnten, können nun plötzlich von der Standardeingabe lesen. Mit ein wenig Übung wurden Befehle, die zuvor unmöglich erschienen, ganz leicht.
Wie die Prozesssubstitution funktioniert
Wenn das Linux-Betriebssystem eine Datei von der Festplatte öffnet, wird diese Datei durch einen Integer-Wert repräsentiert, der Dateideskriptor genannt wird. Die Prozesssubstitution imitiert eine Datei, indem Sie einen Befehl ausführt und dessen Ausgabe mit einem Dateideskriptor verknüpft, sodass die Ausgabe aus Sicht des Programms, das darauf zugreift, in einer Festplattendatei zu liegen scheint. Sie können sich den Dateideskriptor mit echo anschauen:
$ echo <(ls)
/dev/fd/63
In diesem Fall ist der Dateideskriptor für <(ls) 63. Er wird im Systemverzeichnis /dev/fd nachverfolgt.
Fun Fact: Standardeingabe (stdin), Standardausgabe (stdout) und Standardfehlerausgabe (stderr) werden durch die Dateideskriptoren 0, 1 und 2 repräsentiert. Deswegen hat die Umleitung der Standardfehlerausgabe die Syntax 2>.
Der Ausdruck <(…) erzeugt einen Dateideskriptor zum Lesen. Der verwandte Ausdruck >(…) erzeugt einen Dateideskriptor zum Schreiben, allerdings habe ich ihn in 25 Jahren noch nie gebraucht.
Die Prozesssubstitution ist eine Nicht-POSIX-Eigenschaft, die in Ihrer Shell möglicherweise deaktiviert ist. Um Nicht-POSIX-Eigenschaften in Ihrer aktuellen Shell einzuschalten, führen Sie set +o posix aus.
Jeder Befehl ist ein String, aber manche Befehle sind »stringier« als andere. Ich zeige Ihnen hier mehrere Techniken, die Stück für Stück einen String erzeugen und diesen String dann als Befehl ausführen:
Warnung Die folgenden Techniken können riskant sein, weil sie ungesehenen Text zur Ausführung an eine Shell senden. Machen Sie das niemals blind. Versuchen Sie immer, den Text zu verstehen (und seinem Ursprung zu vertrauen), bevor Sie ihn ausführen. Sie wollen schließlich nicht aus Versehen den String "rm -rf $HOME" ausführen und alle Ihre Dateien ausradieren. |
Die bash ist ein normaler Befehl wie andere auch, wie ich in »Shells sind ausführbare Dateien« auf Seite 118 erklärt habe, Sie können sie also über ihren Namen auf der Kommandozeile ausführen. Standardmäßig wird beim Ausführen der bash eine interaktive Shell zum Eintippen und Ausführen von Befehlen gestartet, wie Sie gesehen haben. Alternativ können Sie einen Befehl auch als String an die bash übergeben. Sie nutzen dazu die Option -c. bash führt diesen String als Befehl aus und beendet sich dann:
$ bash -c "ls -l"
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
Wozu ist das gut? Der neue bash-Prozess ist ein Kind mit seiner eigenen Umgebung, einschließlich eines aktuellen Verzeichnisses, Variablen mit Werten und so weiter. Änderungen an der Kind-Shell haben keine Auswirkungen auf Ihre aktuell laufende Shell. Hier ist ein bash -c-Befehl, der das Verzeichnis lange genug auf /tmp ändert, um eine Datei zu löschen, und sich dann beendet:
$ pwd
/home/smith
$ touch /tmp/badfile Erzeugt eine temporäre Datei.
$ bash -c "cd /tmp && rm badfile"
$ pwd
/home/smith Aktuelles Verzeichnis ist unverändert.
Der aufschlussreichste und schönste Anwendungsfall von bash -c tritt jedoch ein, wenn Sie bestimmte Befehle als Superuser ausführen. Speziell die Kombination aus sudo und Eingabe-/Ausgabeumleitung erzeugt eine interessante (manchmal verrückte) Situation, in der bash -c der Schlüssel zum Erfolg ist.
Stellen Sie sich vor, Sie wollten eine Logdatei im Systemverzeichnis /var/log erzeugen, das von normalen Benutzern nicht verändert (geschrieben) werden darf. Sie führen den folgenden sudo-Befehl aus, um Superuser-Rechte zu erlangen, und erzeugen die Logdatei, aber das Ganze scheitert mysteriöserweise:
$ sudo echo "Neue Logdatei" > /var/log/custom.log
bash: /var/log/custom.log: Permission denied
Moment mal! sudo sollte Ihnen das Recht verleihen, irgendwo eine Datei anzulegen. Wie kann dieser Befehl scheitern? Warum hat sudo Sie nicht einmal nach einem Passwort gefragt? Die Antwort: weil sudo gar nicht gelaufen ist. Sie haben sudo auf den echo-Befehl, nicht jedoch auf die Ausgabeumleitung angewandt, die zuerst ausgeführt wurde und fehlgeschlagen ist. Im Einzelnen:
Deshalb wurde sudo niemals ausgeführt. Um dieses Problem zu lösen, müssen Sie der Shell sagen: »Führe den gesamten Befehl als Superuser aus, auch die Ausgabeumleitung.« Das ist exakt die Art von Situation, die bash -c sehr gut beherrscht. Konstruieren Sie den Befehl, den Sie ausführen wollen, als String:
'echo "Neue Logdatei" > /var/log/custom.log'
und übergeben Sie ihn als Argument an sudo bash -c:
$ sudo bash -c 'echo "Neue Logdatei" > /var/log/custom.log'
[sudo] password for smith: xxxxxxxx
$ cat /var/log/custom.log
Neue Logdatei
Dieses Mal haben Sie bash und nicht nur echo als Superuser ausgeführt, und bash führt den gesamten String als Befehl aus. Die Umleitung ist erfolgreich. Denken Sie an diese Technik, wenn Sie sudo mit einer Umleitung kombinieren.
Die Shell liest jeden Befehl, den Sie auf der Standardeingabe eintippen. Das bedeutet, dass das Programm bash an Pipelines teilnehmen kann. Geben Sie zum Beispiel den String "ls -l" aus und leiten Sie ihn in einer Pipe an bash weiter. bash behandelt den String als Befehl und führt ihn aus:
$ echo "ls -l"
ls -l
$ echo "ls -l" | bash
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
Warnung Merken Sie sich, dass Sie niemals blind einen Text in einer Pipe an bash senden sollten. Sie müssen sich immer bewusst sein, was Sie ausführen. |
Diese Technik ist unglaublich, wenn Sie viele ähnliche Befehle hintereinander ausführen müssen. Können Sie die Befehle als Strings ausgeben, dann können Sie die Strings auch als Pipe zur Ausführung an die bash leiten. Angenommen, Sie sind in einem Verzeichnis mit vielen Dateien und wollen diese anhand des ersten Zeichens in Unterverzeichnissen organisieren. Eine Datei namens apple würde in das Unterverzeichnis a verschoben werden, eine Datei namens cantaloupe käme in das Unterverzeichnis c und so weiter.6 (Aus Gründen der Einfachheit gehen wir davon aus, dass alle Dateinamen mit einem Kleinbuchstaben beginnen und keine Leer- oder Sonderzeichen enthalten.)
Zuerst listen Sie die Dateien sortiert auf. Wir nehmen an, dass alle Namen wenigstens zwei Zeichen lang sind (und damit dem Muster ??* entsprechen), sodass unsere Befehle nicht mit den Unterverzeichnissen a bis z kollidieren:
$ ls -1 ??*
apple
banana
cantaloupe
carrot
...
Erzeugen Sie die 26 benötigten Unterverzeichnisse mittels Klammererweiterung:
$ mkdir {a..z}
Nun generieren Sie die mv-Befehle, die Sie brauchen, als Strings. Beginnen Sie mit einem regulären Ausdruck für sed, der das erste Zeichen des Dateinamens als Ausdruck #1 (\1) erfasst:
^\(.\)
Erfassen Sie den Rest des Dateinamens als Ausdruck #2 (\2):
\(.*\)$
Verbinden Sie die beiden regulären Ausdrücke:
^\(.\)\(.*\)$
Formen Sie nun einen mv-Befehl mit dem Wort mv, gefolgt von einem Leerzeichen, dem kompletten Dateinamen (\1\2), einem weiteren Leerzeichen und dem ersten Zeichen (\1):
mv \1\2 \1
Der vollständige Befehlsgenerator ist:
$ ls -1 ??* | sed 's/^\(.\)\(.*\)$/mv \1\2 \1/'
mv apple a
mv banana b
mv cantaloupe c
mv carrot c
...
Seine Ausgabe enthält genau die mv-Befehle, die Sie brauchen. Lesen Sie die Ausgabe, um sicherzustellen, dass sie korrekt ist, indem Sie sie über eine Pipeline an less leiten – so können Sie sie seitenweise betrachten:
Wenn Sie sich davon überzeugt haben, dass Ihre generierten Befehle korrekt sind, leiten Sie die Ausgabe mit einer Pipe zur Ausführung an bash:
$ ls -1 ??* | sed 's/^\(.\)\(.*\)$/mv \1\2\t\1/' | bash
Die Schritte, die Sie gerade abgeschlossen haben, zeigen ein sich wiederholendes Muster:
Hinweis: Diese Technik ist nur dann sinnvoll, wenn Sie mit SSH, der Secure Shell, zum Anmelden an entfernten Hosts vertraut sind. Das Einrichten von SSH-Beziehungen zwischen Hosts kann in diesem Buch nicht behandelt werden, weil es zu weit führen würde. Um mehr darüber zu erfahren, suchen Sie sich ein SSH-Tutorial.
Zusätzlich zu der normalen Methode, sich an einem entfernten Host anzumelden:
$ ssh myhost.example.com
können Sie auch einen einzelnen Befehl auf dem entfernten Host ausführen – indem Sie auf der Kommandozeile einen String an ssh übergeben. Hängen Sie den String einfach an den Rest der ssh-Kommandozeile an:
$ ssh myhost.example.com ls
remotefile1
remotefile2
remotefile3
Diese Technik ist im Allgemeinen schneller, als wenn man sich anmeldet, einen Befehl ausführt und sich wieder abmeldet. Wenn der Befehl Sonderzeichen enthält, etwa Umleitungssymbole, die auf dem entfernten Host ausgewertet werden müssen, setzen Sie sie in Anführungszeichen oder schützen sie durch andere Escape-Symbole. Sonst werden sie nämlich durch Ihre lokale Shell ausgewertet. Beide folgenden Befehle führen ls entfernt aus, allerdings erfolgt die Ausgabeumleitung auf verschiedenen Hosts:
$ ssh myhost.example.com ls > outfile Erzeugt outfile auf dem lokalen Host.
$ ssh myhost.example.com "ls > outfile" Erzeugt outfile auf dem entfernten Host.
Sie können Befehle auch mit einer Pipe an ssh leiten, um sie auf dem entfernten Host auszuführen. Im Prinzip ist das so, als würden Sie sie zur lokalen Ausführung mit einer Pipeline an die bash leiten:
$ echo "ls > outfile" | ssh myhost.example.com
Wenn Sie Befehle mit einer Pipe an ssh leiten, könnte der entfernte Host Diagnose- oder andere Meldungen ausgeben. Diese beeinflussen im Allgemeinen den entfernten Befehl nicht, und Sie können sie unterdrücken:
$ echo "ls > outfile" | ssh -T myhost.example.com
$ echo "ls > outfile" | ssh myhost.example.com bash
Viele Linux-Benutzer haben noch nie von dem Befehl xargs gehört, dabei ist er ein mächtiges Werkzeug zum Konstruieren und Ausführen mehrerer ähnlicher Befehle. Das Erlernen von xargs war ein weiterer entscheidender Augenblick in meiner Linux-Ausbildung. Ich hoffe, das wird für Sie ebenso sein.
xargs nimmt zwei Eingaben entgegen:
xargs verbindet die Eingabestrings und das Befehls-Template, um damit neue, vollständige Befehle zu erzeugen und auszuführen, die ich als generierte Befehle bezeichne. Ich demonstriere dies an einem kleinen Beispiel. Nehmen wir einmal an, dass Sie sich in einem Verzeichnis mit drei Dateien befinden:
$ ls -1
apple
banana
cantaloupe
Schicken Sie die Verzeichnisliste als Pipe an xargs. Sie dient als Eingabestring. Geben Sie außerdem wc -l als Befehls-Template an:
$ ls -1 | xargs wc -l
3 apple
4 banana
1 cantaloupe
8 total
Wie versprochen, wendet xargs das Befehls-Template wc -l auf die Eingabestrings an und zählt die Zeilen in den einzelnen Dateien. Um diese drei Dateien mit cat auszugeben, ändern Sie einfach das Befehls-Template auf »cat«:
$ ls -1 | xargs cat
Mein kleines xargs-Beispiel hat zwei Mängel, einen schlimmen und einen praktischen. Der schlimme Mangel besteht darin, dass xargs etwas Falsches machen könnte, falls ein Eingabestring Sonderzeichen enthält, also etwa Leerzeichen. Eine robuste Lösung finden Sie im Kasten »Sicherheit mit find und xargs« auf Seite 144.
Der praktische Mangel ist, dass Sie xargs hier gar nicht brauchen – Sie könnten die gleiche Aufgabe viel einfacher mit einem Datei-Pattern-Matching erledigen:
$ wc -l *
3 apple
4 banana
1 cantaloupe
8 total
Wozu sollte man dann xargs benutzen? Seine Stärke wird offensichtlich, wenn die Eingabestrings interessanter sind als ein einfaches Verzeichnislisting. Stellen Sie sich vor, Sie möchten (rekursiv) die Zeilen in allen Dateien in einem Verzeichnis und in all seinen Unterverzeichnissen zählen, allerdings nur für Python-Quelldateien mit Namen, die auf .py enden. Es ist leicht, mit find eine solche Liste mit Dateipfaden zu erzeugen:
$ find . -type f -name \*.py -print
fruits/raspberry.py
vegetables/leafy/lettuce.py
...
xargs kann nun das Befehls-Template wc -l auf jeden Dateipfad anwenden, wodurch ein rekursives Ergebnis erzielt wird, das auf anderem Weg nur schwer zu erreichen wäre. Aus Sicherheitsgründen ersetze ich die Option -print durch -print0 und xargs durch xargs -0. Warum ich das mache, erkläre ich im Kasten »Sicherheit mit find und xargs« auf Seite 144:
$ find . -type f -name \*.py -print0 | xargs -0 wc -l
6 ./fruits/raspberry.py
3 ./vegetables/leafy/lettuce.py
...
Durch das Kombinieren von find und xargs können Sie jeden Befehl dazu befähigen, rekursiv durch das Dateisystem zu laufen, wobei nur Dateien (und/oder Verzeichnisse) beeinflusst werden, die Ihren angegebenen Kriterien entsprechen. (In manchen Fällen können Sie die gleiche Wirkung allein mit find erzielen, wenn Sie dessen Option -exec einsetzen, allerdings ist xargs oft eine sauberere Lösung.)
xargs besitzt zahlreiche Optionen (siehe man xargs), die steuern, wie es die generierten Befehle erzeugt und ausführt. Die wichtigsten sind aus meiner Sicht (abgesehen von -0) -n und -I. Die Option -n steuert, wie viele Argumente durch xargs an jeden generierten Befehl angehängt werden. Standardmäßig können so viele Argumente angehängt werden, wie die Shell erlaubt:7
$ ls | xargs echo Nimmt so viele Eingabestrings an wie möglich:
apple banana cantaloupe carrot echo apple banana cantaloupe carrot
$ ls | xargs -n1 echo Ein Argument pro echo-Befehl:
apple echo apple
banana echo banana
cantaloupe echo cantaloupe
carrot echo carrot
$ ls | xargs -n2 echo Zwei Argumente pro echo-Befehl:
apple banana echo apple banana
cantaloupe carrot echo cantaloupe carrot
$ ls | xargs -n3 echo Drei Argumente pro echo-Befehl:
apple banana cantaloupe echo apple banana cantaloupe
carrot echo carrot
Wenn Sie find und xargs kombinieren, dann benutzen Sie xargs -0 (Bindestrich null) statt nur allein xargs, um sich vor unerwarteten Sonderzeichen in den Eingabestrings zu schützen. Kombinieren Sie dies mit der Ausgabe, die durch find -print0 (anstelle von find -print) erzeugt wird:
$ find Optionen... -print0 | xargs -0 Optionen...
Normalerweise erwartet xargs, dass seine Eingabestrings durch Whitespace-Zeichen getrennt werden, also etwa durch Newline-Zeichen. Das ist ein Problem, wenn die Eingabestrings selbst weiteren Whitespace enthalten, wie etwa Dateinamen mit Leerzeichen darin. Standardmäßig behandelt xargs diese Leerzeichen als Eingabetrennzeichen und wird auf unvollständigen Strings tätig, was natürlich falsche Ergebnisse erzeugt. Falls zum Beispiel die Eingabe für xargs eine Zeile wie prickly pear.py enthält, behandelt xargs dies wie zwei Eingabestrings, und Sie erhalten wahrscheinlich eine solche Fehlermeldung:
prickly: No such file or directory
pear.py: No such file or directory
Um dieses Problem zu vermeiden, benutzen Sie xargs -0 (das ist eine Null), um ein anderes Zeichen als Eingabetrennzeichen zu akzeptieren, nämlich das Null-Zeichen (ASCII-Null). Nullen tauchen nur selten in Text auf, sind also ideale, eindeutige Trennzeichen für Eingabestrings.
Wie können Sie nun Ihre Eingabestrings mit Nullen statt mit Newline-Zeichen trennen? Zum Glück besitzt find eine Option, die genau dies tut:-print0 statt -print.
Der ls-Befehl bietet leider keine Option, um seine Ausgabe mit Nullen zu trennen, sodass meine früher gezeigten Beispiele mit ls nicht sicher sind. Sie können Newlines mit tr in Nullen verwandeln:
$ ls | tr '\n' '\0' | xargs -0 ...
Oder Sie benutzen diesen praktischen Alias, der das aktuelle Verzeichnis auflistet, wobei die Einträge durch Nullen getrennt sind. Das eignet sich gut für eine Weiterleitung mittels einer Pipeline an xargs:
alias ls0="find . -maxdepth 1 -print0"
Die Option -I kontrolliert, wo die Eingabestrings in dem generierten Befehl auftauchen. Standardmäßig werden sie an das Befehls-Template angehängt, aber Sie können sie auch anderswo erscheinen lassen. Hinter -I setzen Sie einen String (Ihrer Wahl), und dieser String wird zu einem Platzhalter im Befehls-Template, der genau anzeigt, wo Eingabestrings eingefügt werden sollten:
$ ls | xargs -
I XYZ echo XYZ is my favorite food Benutzen Sie XYZ als Platzhalter.
apple is my favorite food
banana is my favorite food
cantaloupe is my favorite food
carrot is my favorite food
Ich habe »XYZ« als Platzhalter für die Eingabestrings gewählt und unmittelbar hinter echo positioniert. Dadurch kommt der Eingabestring direkt an den Anfang jeder ausgegebenen Zeile. Beachten Sie, dass die Option -I den Befehl xargs auf einen Eingabestring pro generierten Befehl begrenzt. Ich empfehle Ihnen, die xargs-Manpage gründlich zu lesen, um herauszufinden, was Sie noch alles kontrollieren können.
Lange Argumentlisten xargs ist ein echter Schatz, wenn Befehlszeilen sehr lang werden. Nehmen wir einmal an, Ihr aktuelles Verzeichnis enthielte eine Million Dateien namens file1.txt bis file1000000.txt und Sie versuchten, sie mittels Pattern Matching zu entfernen: |
|
|
$ rm *.txt bash: /bin/rm: Argument list too long |
|
Das Muster *.txt wird zu einem String mit mehr als 14 Millionen Zeichen ausgewertet, was länger ist als das, was Linux unterstützt. Um diese Beschränkung zu umgehen, schicken Sie in einer Pipeline eine Liste mit Dateien zum Löschen an xargs. xargs teilt die Dateiliste auf mehrere rm-Befehle auf. Sie bilden die Liste aus Dateien, indem Sie in einer Pipeline ein vollständiges Verzeichnislisting an grep senden, wobei Sie nur Dateinamen erfassen, die auf .txt enden, und das dann an xargs leiten: |
|
$ ls | grep '\.txt$' | xargs rm |
|
Diese Lösung ist besser als das Datei-Pattern-Matching (ls *.txt), das den gleichen Argument list too long-Fehler erzeugt. Besser noch, führen Sie find -print0 aus, wie in »Sicherheit mit find und xargs« auf Seite 144 beschrieben: |
|
$ find . -maxdepth 1 -name \*.txt -type f -print0 \ | xargs -0 rm |
Bisher belegen alle Befehle, die ich besprochen habe, die Parent-Shell, bis sie fertig sind. Schauen wir uns einige Techniken an, die eine andere Beziehung mit der Parent-Shell aufbauen:
Hintergrundbefehle
Der Prompt wird sofort zurückgegeben, und der Befehl wird irgendwo hinter den Kulissen ausgeführt.
Explizite Subshells
Können mitten in einem kombinierten Befehl gestartet werden.
Prozessersetzung
Tritt an die Stelle der Parent-Shell.
Bisher bringen all unsere Techniken einen Befehl zum Abschluss, während Sie warten, und präsentieren dann den nächsten Shell-Prompt. Dabei müssen Sie jedoch nicht warten, vor allem nicht, wenn Befehle sehr lange brauchen. Sie können Befehle auf eine besondere Art und Weise starten, sodass sie quasi aus dem Blick verschwinden, aber dennoch weiterlaufen, sodass die Shell sofort wieder frei für die nächsten Befehle wird. Diese Technik wird als Hintergrundausführung eines Befehls bezeichnet. Im Gegensatz dazu werden Befehle, die die Shell belegen, Vordergrundbefehle genannt. Eine Shell-Instanz führt zu einem Zeitpunkt höchstens einen Vordergrundbefehl sowie eine beliebige Anzahl an Hintergrundbefehlen aus.
Um einen Befehl im Hintergrund aufzurufen, hängen Sie einfach ein Ampersand-Zeichen (&) an (auch Kaufmanns-Und genannt). Die Shell reagiert mit einer kryptisch aussehenden Nachricht, die anzeigt, dass der Befehl im Hintergrund läuft, und zeigt dann den nächsten Prompt an:
$ wc -c mein_extrem_riesiges_file.txt & Zählt die Zeichen in einer riesigen Datei.
[1] 74931 Kryptisch aussehende Antwort.
$
Sie können dann in dieser Shell weitere Vordergrundbefehle (oder weitere Hintergrundbefehle) ausführen. Die Ausgabe der Hintergrundbefehle könnte jederzeit auftauchen, selbst wenn Sie gerade tippen. Falls der in den Hintergrund geschobene Befehl erfolgreich beendet wurde, informiert die Shell Sie mit der Meldung Done:
59837483748 mein_extrem_riesiges_file.txt
[1]+ Done wc -c mein_extrem_riesiges_file.txt
Schlägt der Befehl fehl, erhalten Sie eine Exit-Meldung mit einem Exit-Code:
[1]+ Exit 1 wc -c mein_extrem_riesiges_file.txt
Tipp Das Ampersand-Zeichen ist auch ein Listenoperator, genau wie && und ||: |
|
$ befehl1 & befehl2 & befehl3 & alle 3 Befehle [1] 57351 im Hintergrund [2] 57352 [3] 57353 $ befehl4 & befehl5 & echo hi alle im Hintergrund [1] 57431 bis auf "echo" [2] 57432 hi |
Eine verwandte Technik ist, einen Befehl als Vordergrundbefehl auszuführen, dann seine Meinung zu ändern und ihn in den Hintergrund zu schicken. Drücken Sie Strg-Z, um den Befehl zeitweise zu stoppen (dies wird als Suspendieren des Befehls bezeichnet) und zum Shell-Prompt zurückzukehren; tippen Sie dann bg ein, um die Ausführung des Befehls wieder aufzunehmen – nun aber im Hintergrund.
Hintergrundbefehle sind Teil einer Shell-Eigenschaft namens Jobkontrolle. Diese manipuliert Befehle auf verschiedene Arten, etwa durch Hintergrundausführung, Suspendierung und Wiederaufnahme der Ausführung. Ein Job ist die Arbeitseinheit einer Shell: eine einzelne Instanz eines Befehls, die in einer Shell abläuft. Jobs sind zum Beispiel einfache Befehle, Pipelines und bedingte Listen – im Prinzip alles, was Sie auf der Kommandozeile ausführen können.
Ein Job ist mehr als ein Linux-Prozess. Ein Job kann aus einem Prozess, zwei Prozessen oder mehr bestehen. Eine Pipeline aus sechs Programmen ist zum Beispiel ein einzelner Job, der (wenigstens) sechs Prozesse umfasst. Jobs sind ein Konstrukt der Shell. Das Linux-Betriebssystem behält nicht die Jobs im Auge, sondern die zugrunde liegenden Prozesse.
Eine Shell kann jederzeit mehrere Jobs ausführen. Jeder Job in einer bestimmten Shell hat eine positive Integer-ID, die sogenannte Job-ID oder Jobnummer. Wenn Sie einen Befehl im Hintergrund aufrufen, gibt die Shell die Jobnummer und die ID des ersten Prozesses aus, den sie in dem Job ausführt. Im folgenden Befehl ist 1 die Jobnummer, und die Prozess-ID ist 74931:
$ wc -c mein_extrem_riesiges_file.txt &
[1] 74931
In die Shell sind Befehle zur Jobkontrolle eingebaut; Sie sehen diese Befehle in Tabelle 7-1. Ich demonstriere die gebräuchlichsten Operationen zur Jobkontrolle, indem ich einige Jobs ausführe und sie manipuliere. Um die Jobs einfach und vorhersehbar zu halten, rufe ich den Befehl sleep auf, der für ein paar Sekunden einfach nur da ist und nichts macht (»schläft«) und sich dann beendet. Zum Beispiel wird bei sleep 10 für 10 Sekunden geschlafen.
Tabelle 7-1: Befehle zur Jobkontrolle
Befehl |
Bedeutung |
bg |
Verschiebt den momentan suspendierten Job in den Hintergrund. |
bg %n |
Verschiebt den suspendierten Job mit der Nummer n in den Hintergrund (Beispiel: bg %1). |
fg |
Verschiebt den aktuellen Hintergrundjob in den Vordergrund. |
fg %n |
Verschiebt den Hintergrundjob mit der Nummer n in den Vordergrund (Beispiel: fg %2). |
kill %n |
Terminiert den Hintergrundjob mit der Nummer n (Beispiel: kill %3). |
jobs |
Zeigt die Jobs einer Shell. |
Führen Sie einen Job im Hintergrund bis zur Fertigstellung aus:
$ sleep 20 & Ausführen im Hintergrund
[1] 126288
$ jobs Auflisten der Jobs dieser Shell
[1]+ Running sleep 20 &
$
...schließlich...
[1]+ Done sleep 20
Hinweis Wenn Jobs fertig sind, kann es sein, dass die Meldung Done erst dann auftaucht, wenn Sie das nächste Mal Enter drücken. |
Führen Sie einen Hintergrundjob aus und bringen Sie ihn in den Vordergrund:
$ sleep 20 & Ausführen im Hintergrund
[1] 126362
$ fg in den Vordergrund bringen
sleep 20
...schließlich...
$
Führen Sie einen Vordergrundjob aus, suspendieren Sie ihn und bringen Sie ihn wieder in den Vordergrund:
$ sleep 20 Ausführen im Vordergrund
^Z Suspendieren des Jobs
[1]+ Stopped sleep 20
$ jobs Auflisten der Jobs dieser Shell
[1]+ Stopped sleep 20
$ fg in den Vordergrund bringen
sleep 20
...schließlich...
[1]+ Done sleep 20
Führen Sie einen Vordergrundjob aus und schicken Sie ihn in den Hintergrund:
$ sleep 20 Ausführen im Vordergrund
^Z Suspendieren des Jobs
[1]+ Stopped sleep 20
$ bg Verschieben in den Hintergrund
[1]+ sleep 20 &
$ jobs Auflisten der Jobs dieser Shell
[1]+ Running sleep 20 &
$
...schließlich...
[1]+ Done sleep 20
Arbeiten Sie mit mehreren Hintergrundjobs. Verweisen Sie auf einen Job mit der Jobnummer, der ein Prozentzeichen vorangestellt ist (%1, %2 und so weiter):
$ sleep 100 & Ausführen von 3 Befehlen im Hintergrund
[1] 126452
$ sleep 200 &
[2] 126456
$ sleep 300 &
[3] 126460
$ jobs Auflisten der Jobs dieser Shell
[1] Running sleep 100 &
[2]- Running sleep 200 &
[3]+ Running sleep 300 &
$ fg %2 Job 2 in den Vordergrund bringen
sleep 200
^Z Suspendieren von Job 2
[2]+ Stopped sleep 200
$ jobs Feststellen, ob Job 2 suspendiert ("stopped")
[1] Running sleep 100 &
[2]+ Stopped sleep 200
[3]- Running sleep 300 &
$ kill %3 Terminieren von Job 3
[3]+ Terminated sleep 300
$ jobs Feststellen, ob Job 3 verschwunden ist
[1]- Running sleep 100 &
[2]+ Stopped sleep 200
$ bg %2 Suspendierten Job 2 im Hintergrund aufnehmen
[2]+ sleep 200 &
$ jobs Feststellen, ob Job 2 wieder läuft
[1]- Running sleep 100 &
[2]+ Running sleep 200 &
$
Ein in den Hintergrund geschobener Befehl könnte auf die Standardausgabe schreiben, und das manchmal auch zu Zeiten, an denen uns das eigentlich nicht passt. Beachten Sie, was passiert, wenn Sie im Hintergrund die Linux-Dictionary-Datei sortieren (diese ist 100.000 Zeilen lang) und die ersten beiden Zeilen ausgeben lassen. Wie erwartet, gibt die Shell sofort die Jobnummer (1), eine Prozess-ID (81089) und den nächsten Prompt aus:
$ sort /usr/share/dict/words | head -n2 &
[1] 81089
$
Falls Sie warten, bis der Job beendet ist, gibt sie zwei Zeilen auf der Standardausgabe aus –, genau an der Stelle, an der sich der Cursor zu diesem Zeitpunkt befindet. In diesem Fall sitzt der Cursor am zweiten Prompt, Sie erhalten also diese etwas schlampig aussehende Ausgabe:
$ sort /usr/share/dict/words | head -n2 &
[1] 81089
$ A
A's
Drücken Sie Enter, und die Shell gibt eine »Job erledigt«-Meldung (Done) aus:
[1]+ Done sort /usr/share/dict/words | head -n2
$
Die Bildschirmausgabe eines Hintergrundjobs kann jederzeit auftauchen, während der Job läuft. Um diese Art von Chaos zu vermeiden, leiten Sie die Standardausgabe in eine Datei um. Schauen Sie sich diese dann an, wenn es Ihnen passt und Sie Zeit dafür haben:
$ sort /usr/share/dict/words | head -n2 > /tmp/results &
[1] 81089
$
[1]+ Done sort /usr/share/dict/words | head -n2 > /tmp/results
$ cat /tmp/results
A
A's
$
Andere seltsame Dinge geschehen, wenn ein Hintergrundjob versucht, von der Standardeingabe zu lesen. Die Shell suspendiert den Job, gibt eine Stopped-Meldung aus und wartet im Hintergrund auf eine Eingabe. Probieren Sie das einmal aus, indem Sie cat ohne Argumente in den Hintergrund schieben, sodass es die Standardeingabe liest:
Jobs können im Hintergrund keine Eingaben lesen. Bringen Sie den Job deshalb mit fg in den Vordergrund und versorgen Sie ihn dann mit der Eingabe:
$ fg
cat
Hier ist eine Eingabe
Hier ist eine Eingabe
...
Nachdem Sie alle Eingaben geliefert haben, machen Sie eines der folgenden Dinge:
Die Hintergrundausführung ist ideal für Befehle, die lange laufen, wie etwa Texteditoren während langer Bearbeitungssessions oder alle Programme, die ihre eigenen Fenster öffnen. So können zum Beispiel Programmierer eine Menge Zeit sparen, indem sie ihren Texteditor suspendieren, anstatt ihn zu beenden. Ich habe gesehen, wie erfahrene Ingenieure Code in ihrem Texteditor bearbeiten, dann speichern und den Editor beenden, den Code testen, anschließend den Editor neu starten und nach der Stelle im Code suchen, an der sie zuletzt gearbeitet haben. Sie verlieren jedes Mal, wenn sie den Editor beenden, 10 bis 15 Sekunden für den Jobwechsel. Würden sie stattdessen den Editor suspendieren (Strg-Z), ihren Code testen und den Editor dann wieder reaktivieren (fg), verplempern sie viel weniger Zeit.
Die Hintergrundausführung ist darüber hinaus großartig, wenn man eine Abfolge von Befehlen mithilfe einer bedingten Liste im Hintergrund ausführen möchte. Falls einer der Befehle in der Liste scheitert, wird der Rest nicht ausgeführt, und der Job wird beendet. (Achten Sie nur auf Befehle, die eine Eingabe lesen, da diese dafür sorgen, dass der Job pausiert und auf just diese Eingabe wartet.)
$ befehl1 && befehl2 && befehl3 &
Immer wenn Sie einen einfachen Befehl starten, läuft dieser in einem Kindprozess, wie Sie in »Eltern- und Kindprozesse« auf Seite 119 gesehen haben. Befehlssubstitution und Prozesssubstitution erzeugen Subshells. Es gibt jedoch Zeiten, an denen es hilfreich ist, explizit eine zusätzliche Subshell zu starten. Schließen Sie dazu einfach einen Befehl in runde Klammern ein. Der Befehl wird dann in einer Subshell ausgeführt:
$ (cd /usr/local && ls)
bin etc games lib man sbin share
$ pwd
/home/smith "cd /usr/local" lief in einer Subshell.
Auf einen ganzen Befehl angewandt, ist diese Technik nicht besonders sinnvoll, außer dass Sie vielleicht nicht noch einen zweiten cd-Befehl ausführen müssen, um zu Ihrem vorherigen Verzeichnis zurückzukehren. Falls Sie jedoch die runden Klammern um einen Teil eines kombinierten Befehls setzen, können Sie einige ganz nützliche Tricks ausführen. Ein typisches Beispiel ist eine Pipeline, die mitten in der Ausführung das Verzeichnis wechselt. Nehmen wir einmal an, Sie hätten eine komprimierte tar-Datei heruntergeladen, package.tar.gz, und möchten die Dateien auspacken. Ein tar-Befehl zum Extrahieren der Dateien lautet:
$ tar xvf package.tar.gz
Makefile
src/
src/defs.h
src/main.c
...
Die Extraktion erfolgt relativ zum aktuellen Verzeichnis.8 Was ist, wenn Sie die Dateien in ein anderes Verzeichnis extrahieren wollen? Sie könnten zuerst mit cd in das andere Verzeichnis wechseln und dann tar ausführen (und anschließend wieder mit cd zurückwechseln), aber Sie können diese Aufgabe auch mit einem einzigen Befehl erledigen. Der Trick besteht darin, die gepackten Daten mit einer Pipeline an eine Subshell zu leiten, die die Verzeichnisoperationen ausführt und tar laufen lässt, während sie von der Standardeingabe liest:9
$ cat package.tar.gz | (mkdir -p /tmp/other && cd /tmp/other && tar xzvf -)
Diese Technik funktioniert auch für das Kopieren von Dateien von einem Verzeichnis dir1 in ein anderes existierendes Verzeichnis dir2 mithilfe zweier tar-Prozesse, von denen einer auf die Standardausgabe schreibt und einer von der Standardeingabe liest:
$ tar czf - dir1 | (cd /tmp/dir2 && tar xvf -)
Die gleiche Technik kann Dateien über SSH in ein vorhandenes Verzeichnis auf einem anderen Host kopieren:
$ tar czf - dir1 | ssh myhost '(cd /tmp/dir2 && tar xvf -)'
Welche Techniken erzeugen Subshells?
Viele der Techniken in diesem Kapitel starten eine Subshell, die die Umgebung der Eltern-Shell (Variablen und deren Werte) sowie weiteren Shell-Kontext wie etwa Aliase erbt. Andere Techniken starten nur einen Kindprozess. Die einfachste Methode, sie zu unterscheiden, besteht darin, die Variable BASH_SUBSHELL auszuwerten, die für eine Subshell ungleich null, ansonsten aber null ist. Weitere Details finden Sie in »Kind-Shells versus Subshells« auf Seite 124.
$ echo $BASH_SUBSHELL normale Ausführung
0 keine Subshell
$ (echo $BASH_SUBSHELL) explizite Subshell
1 Subshell
$ echo $(echo $BASH_SUBSHELL) Befehlssubstitution
1 Subshell
$ cat <(echo $BASH_SUBSHELL) Prozesssubstitution
1 Subshell
$ bash -c 'echo $BASH_SUBSHELL' bash -c
0 keine Subshell
Warnung Es ist verlockend, bash-Klammern so zu sehen, als würden sie einfach Befehle zusammenfassen, also wie die Klammern in der Mathematik. Das machen sie jedoch nicht. Jedes Paar aus runden Klammern sorgt dafür, dass eine Subshell gestartet wird. |
Wenn Sie normalerweise einen Befehl starten, führt die Shell ihn in einem separaten Prozess aus, der zerstört wird, wenn der Befehl fertig ist. Ich habe das in »Eltern- und Kindprozesse« auf Seite 119 beschrieben. Sie können dieses Verhalten mit dem Befehl exec ändern, einem Shell-Builtin. Der Befehl ersetzt die laufende Shell (einen Prozess) durch einen anderen Befehl Ihrer Wahl (einen anderen Prozess). Wenn der neue Befehl beendet wird, folgt kein Shell-Prompt, weil die Original-Shell weg ist.
Um das zu demonstrieren, führen Sie manuell eine neue Shell aus und ändern deren Prompt:
$ bash Ausführen einer Kind-Shell
$ PS1="Doomed> " Ändern des Prompts der neuen Shell
Doomed> echo hello Ausführen irgendeines Befehls
hello
Führen Sie nun mit exec einen Befehl aus und beobachten Sie, wie die neue Shell stirbt:
Doomed> exec ls ls ersetzt die Kind-Shell, läuft und beendet sich.
animals.txt
$ Ein Prompt von der Original-(Eltern-)Shell.
Warum sollte man exec ausführen? Ein Grund ist, Ressourcen zu sparen, indem man keinen zweiten Prozess startet. Shell-Skripte machen manchmal von dieser Optimierung Gebrauch, indem sie exec auf dem letzten Befehl im Skript ausführen. Wenn das Skript sehr oft ausgeführt wird (wir reden hier von Millionen oder Milliarden von Ausführungen), lohnen sich die Einsparungen vermutlich durchaus.
exec besitzt noch eine zweite Fähigkeit – es kann die Standardeingabe, die Standardausgabe und/oder die Standardfehlerausgabe für die aktuelle Shell neu zuweisen. Das ist am praktischsten in einem Shell-Skript, wie unser kleines Beispiel zeigt, das Informationen in eine Datei, /tmp/outfile, ausgibt:
#!/bin/bash
echo "Mein Name ist $USER" > /tmp/outfile
echo "Mein aktuelles Verzeichnis ist $PWD" >> /tmp/outfile
echo "Rate, wie viele Zeilen in der Datei /etc/hosts sind?" >> /tmp/outfile
wc -l /etc/hosts >> /tmp/outfile
echo "Tschüss erstmal" >> /tmp/outfile
Anstatt die Ausgabe der jeweiligen Befehle einzeln nach /tmp/outfile umzuleiten, führen Sie exec aus, um die Standardausgabe für das gesamte Skript nach /tmp/outfile umzuleiten. Nachfolgende Befehle können einfach auf die Standardausgabe ausgeben:
#!/bin/bash
# Umleiten der Standardausgabe für dieses Skript.
exec > /tmp/outfile2
# Alle nachfolgenden Befehle geben nach /tmp/outfile2 aus.
echo "Mein Name ist $USER"
echo "Mein aktuelles Verzeichnis ist $PWD"
echo "Rate, wie viele Zeilen in der Datei /etc/hosts sind?"
wc -l /etc/hosts
echo "Tschüss erstmal"
Führen Sie dieses Skript aus und schauen Sie sich in der Datei /tmp/outfile2 die Ergebnisse an:
$ cat /tmp/outfile2
Mein Name ist smith
Mein aktuelles Verzeichnis ist /home/smith
Rate, wie viele Zeilen in der Datei /etc/hosts sind?
122 /etc/hosts
Tschüss erstmal
Sie werden exec wahrscheinlich nicht oft verwenden, aber es ist da, wenn Sie es brauchen.
Sie verfügen nun über 13 Techniken zum Ausführen eines Befehls – über die 11 aus diesem Kapitel sowie über einfache Befehle und Pipelines. Tabelle 7-2 fasst noch einmal einige übliche Anwendungsfälle für verschiedene Techniken zusammen.
Tabelle 7-2: Gebräuchliche Möglichkeiten zum Ausführen von Befehlen
Problem |
Lösung |
Standardausgabe eines Programms zur Standardeingabe eines anderen senden |
Pipelines |
Einsetzen der Ausgabe (Standardausgabe) in einen Befehl |
Befehlssubstitution |
Bereitstellen der Ausgabe (Standardausgabe) für einen Befehl, der nicht von der Standardeingabe liest, aber Festplattendateien lesen kann |
Prozesssubstitution |
Ausführen eines Strings als Befehl |
bash -c oder Pipeline an bash |
Ausgeben mehrerer Befehle auf der Standardausgabe und Ausführen dieser Befehle |
Pipeline an bash |
Ausführen mehrerer ähnlicher Befehle hintereinander |
xargs oder Konstruieren dieser Befehle als Strings und Pipeline an bash |
Verwalten von Befehlen, die vom Erfolg eines anderen Befehls abhängen |
bedingte Listen |
Ausführen mehrerer Befehle gleichzeitig |
Hintergrundausführung |
Ausführen mehrerer Befehle gleichzeitig, die vom Erfolg eines anderen Befehls abhängen |
Hintergrundausführung einer bedingten Liste |
Ausführen eines Befehls auf einem entfernten Host |
Ausführen von ssh host befehl |
Ändern des Verzeichnisses in der Mitte einer Pipeline |
explizite Subshells |
Späteres Ausführen eines Befehls |
bedingungslose Liste mit sleep, gefolgt von dem Befehl |
Umleiten an/von geschützten Dateien |
Ausführen von sudo bash -c "befehl > datei |
In den nächsten beiden Kapiteln lernen Sie, wie Sie Techniken kombinieren können, um effizient Ihre Ziele zu erreichen.