Bash Tricks
Auf dieser Seite sammle ich nützliche Codeschnipsel, auf die man manchmal nicht so schnell kommt ;-)
Der Text zum Bash-Prompt steht jetzt übrigens auf einer eigenen Seite.
Variablen aus der Subshell
Ein eigentlich unlösbares Problem: Programme laufen in einer Subshell oder einer Pipe, und man hätte gern die Exitcodes der Programme vorn in der Pipe. Geht nicht? Geht doch. Mit der folgenden Lösung stehen die Exitcodes hinterher als Variablen zur Verfügung. Neben den Exitcodes kann man natürlich auch andere Daten in Variablen packen.
Ich gehe von folgender Befehlszeile aus:
grep Mist /var/log/messages | tail -n 10 | cut -d' ' -f3-
Daraus wird dann das folgende Script:
exec 3>&1 # 3. Ausgabekanal öffnen (für STDOUT) exec 4>&1 # 4. Ausgabekanal öffnen (für die Exitcodes) eval ` # >-- Backtick auf! { { grep Mist /var/log/messages echo "grepexit=$?" >&4; } | { tail -n 10 echo "tailexit=$?" >&4 echo "foobar='This is a test.'" >&4 } | cut -d' ' -f3- } 4>&1 >&3 # Umleitung ` # >-- Backtick zu! echo "nach Scriptende:" echo "grepexit $grepexit" echo "tailexit $tailexit" echo "foobar $foobar"
Ausgabe auf der Konsole:
Einige Erklärungen, wie das Script funktioniert:nach Scriptende: grepexit 1 tailexit 0 foobar This is a test.
- Die geschweiften Klammern bilden jeweils eine "Command Group" (siehe Compound Commands in man bash.)
- Die äußere Klammerebene (direkt nach dem Öffnen des Backticks bis zum Schließen der Backticks) sorgt dafür, dass die gesamte Ausgabe des dazwischenliegenden Codeblocks zwischen den Ausgabekanälen rotiert wird.
- Die inneren Klammerebenen bilden jeweils einen "Befehl" und ermöglichen es so, innerhalb der Pipe mehrere Befehle auszuführen.
- Die zu exportierenden Variablen werden jeweils auf den Ausgabekanal 4 geschrieben, und zwar in Form eines Bash-Befehls. Vor dem Schließen der Backticks wird STDOUT auf Kanal 3 geleitet und die Variablen von Kanal 4 auf STDOUT. Anschließend wird STDOUT durch eval ausgewertet und setzt so die Variablen. War doch leicht, oder? ;-)
Übrigens: In meiner /var/log/messages steht kein Mist ;-)
Nochmal Subshell
Die obige Methode ist zu kompliziert? Für einfache Fälle gibt es auch eine einfachere Lösung, nämlich die geschickte Ausweitung der Subshell ;-)
Der Nachteil dieser Methode ist allerdings, dass nur die Werte aus der letzten Subshell (hinter der Pipe) verfügbar sind. Im folgenden Beispiel ist also nur der Wert von $counter verfügbar, nicht aber beispielsweise der Exitcode des Befehls vor der Pipe. Außerdem ist nach dem Schließen der geschweiften Klammer $counter wieder weg.
Im folgenden Beispiel führe ich einen bash-internen Ersatz für wc -l vor.
counter=0 # Variable initialisieren echo -e "a\nb\nc\nd" | { # ^----- hier startet die Subshell while read line ; do counter=$(($counter+1)) done echo "Anzahl (innerhalb der Subshell): $counter" } # erst hier endet die Subshell! echo "hinter der geschweiften Klammer: $counter"
Ausgabe auf der Konsole:
Anzahl (innerhalb der Subshell): 4 hinter der geschweiften Klammer: 0
Wie man sieht, hat $counter nach dem Schließen der geschweiften Klammer wieder den Wert, der ganz am Anfang (außerhalb der Subshell) zugewiesen wurde.
Lesetipp: man bash - hier insbesondere unter den Stichpunkten Compound Commands (Ausweitung der Subshell) und Arithmetic Expansion (Rechenfunktionen).
Nur um es klarzustellen: Die Subshell wird durch die Pipe gestartet, die geschweiften Klammern sind lediglich eine "command group", starten aber keine eigene Subshell.
Von hinten durch die Brust ins Auge...
Alexander Dalloz hat mir eine Variante geschickt, die das Ganze umdreht und die Subshell hinter die Schleife packt. Das bedeuted im Endeffekt, dass der interessante Teil in der Hauptshell läuft und die Variablen problemlos verfügbar sind.
counter=0 while read line; do counter=$(($counter+1)) done < <(echo -e "a\nb\nc\nd") echo "Anzahl: $counter"
... und nochmal ;-)
Lars Ellenberg hat mich darauf hingewiesen, dass man noch leichter an die Exitcodes aus der Pipe kommen kann:
false | true | false | true | false ; \ echo "${PIPESTATUS[*]}" 1 0 1 0 1
In einem Script kann man das ; \ am Ende der ersten Zeile weglassen, es ist nur zum Testen in einer interaktiven Shell nötig.
Geht es nur darum, den Fehlschlag eines beliebigen Befehls in der Pipe zu erkennen, hilft set -o pipefail (Hinweis von Torsten Foertsch):
{ true|false|true; } || echo failed set -o pipefail { true|false|true; } || echo failed failed
Wenn man mehr als nur die Exitcodes (also beliebige Variablen) braucht, muss man allerdings wieder auf meine obigen Konstruktionen zugreifen ;-)
Letzte Aktualisierung: 17.1.2006