KAPITEL 7

Elf weitere Möglichkeiten, einen Befehl auszuführen

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.

Listentechniken

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.

Technik #1: Bedingte Listen

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

$ cd dir || mkdir dir

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.

Technik #2: Bedingungslose Listen

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"

Substitutionstechniken

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).

Technik #3: Befehlssubstitution

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.

image

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"

Technik #4: Prozesssubstitution

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.

Befehl-als-String-Techniken

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:

image

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.

Technik #5: Übergeben eines Befehls als Argument an die bash

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:

  1. Sie haben Enter gedrückt.
  2. Die Shell begann, den ganzen Befehl auszuwerten, einschließlich der Umleitung (>).
  3. Die Shell versuchte, in einem geschützten Verzeichnis, nämlich /var/log, die Datei custom.log anzulegen.
  4. Sie hatten keine Berechtigung, in /var/log zu schreiben, weshalb die Shell aufgab und die Meldung Permission denied ausgab.

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.

Technik #6: Einen Befehl mit einer Pipeline an bash leiten

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

image

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:

$ ls -1 ??* | sed 's/^\(.\)\(.*\)$/mv \1\2\t\1/' | less

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:

  1. Ausgeben einer Sequenz aus Befehlen durch das Manipulieren von Strings.
  2. Betrachten der Ergebnisse mit less, um die Richtigkeit zu prüfen.
  3. Leiten der Ergebnisse mit einer Pipe an bash.

Technik #7: Entferntes Ausführen eines Strings mit ssh

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

Technik #8: Ausführen einer Liste von Befehlen mit xargs

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

Sicherheit mit find und xargs

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.

image

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

Prozesskontrolltechniken

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.

Technik #9: Einen Befehl in den Hintergrund schieben

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.

Einen Befehl im Hintergrund starten

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

image

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

Einen Befehl suspendieren und in den Hintergrund schicken

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.

Jobs und Jobkontrolle

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

Gebräuchliche Operationen zur Jobkontrolle

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

image

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 &

$

Ausgabe und Eingabe im Hintergrund

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:

$ cat &

[1] 82455

[1]+ Stopped cat

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:

Tipps für die Hintergrundausführung

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 &

Technik #10: Explizite Subshells

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

image

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.

Technik #11: Prozessersetzung

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.

image

Das Ausführen von exec kann tödlich sein

Falls Sie exec in einer Shell ausführen, beendet sich die Shell anschließend. Lief die Shell in einem Terminalfenster, schließt sich das Fenster. War die Shell eine Login-Shell, werden Sie ausgeloggt.

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.

Zusammenfassung

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.