Open Source im professionellen Einsatz

Shellskripte aus der Stümper-Liga - Folge 10: Subshells und Deklarationen

Bash Bashing

Kinder verheimlichen manches ihren Eltern. Das gilt auch für die Subshell in der Bash. Die Shellfunktion ist da mitteilsamer: Anders als ihre Schwester offenbart sie ihre Variablen. Verwechslungsgefahr droht!

Solange die Shell nur eine Reihe Kommandos ausführt, ist für Bash-Programmierer die Welt noch in Ordnung. In diesem Fall bewegt sich das Skript auf einer einzigen Ebene, auf der sich insbesondere Variablen alle gleich verhalten. Einige Schleifen, Subshells und Shellfunktionen bringen jedoch Irritationen ins Leben des Entwicklers.

Zunächst einmal lohnt der Blick auf die Variablen selbst. Jede von ihnen hat einen Namen und einen Wert. Typen hingegen kennt die Bash nur in eingeschränkter Weise und nennt sie deshalb wohl auch bloß Attribute [1]. Um sich alle Attribute aller Variablen anzusehen, benutzt der Anwender »declare -p«. Interessant ist das Interger-Attribut, das dafür sorgt, dass die Bash Zuweisungen an diese Variable arithmetisch auswertet (siehe Abbildung 1).

Abbildung 1: Die Bash expandiert Zuweisungen an Variablen mit Integer-Attribut arithmetisch.

Abbildung 1: Die Bash expandiert Zuweisungen an Variablen mit Integer-Attribut arithmetisch.

Bei der ersten Zuweisung hat die Variable »demo« noch keine Sondereigenschaften. Durch das »declare -i demo« ändert sich zwar nicht der bestehende Wert, aber bei künftigen Zuweisungen expandiert die Shell arithmetische Ausdrücke und macht so aufwändige Konstrukte mittels »expr« oder »(( ))« überflüssig. Weitere Attribute sind »-a« für Arrays oder »-r« für Variablen, die danach nicht mehr durch Zuweisungen veränderbar sind. Ein Alias für »declare« ist »typeset«.

Geltungsbereiche

Unabhängig von den Attributen ist jedoch der so genannte Scope, also die Gültigkeit der Variablen in gewissen Kontexten. Dazu zählen insbesondere Subshells und Funktionen. Eine Subshell ist eine Instanz der Shell in sich selbst. Solch eine Subshell ist etwa dann nützlich, wenn der Programmierer die Ausgabe zweier Programme oder Quellen zusammenführen möchte, um sie anschließend mit einem einzigen Tool weiterzuverarbeiten.

Listing 1 sucht per Grep nach den Nameservern und stellt dem Ergebnis in Zeile 2 eine Überschrift voran. Beide Ergebnisse soll das Skript durch das Sed-Kommando in Zeile 4 gesperrt ausgeben. Wegen der runden Klammern entsteht eine Subshell, die ihre Ausgabe uniform in den Sed-Aufruf führt.

Listing 1: Subshell

01 #!/bin/sh
02 (echo "Meine DNS-Konfiguration:";
03  grep "nameserver" /etc/resolv.conf) |
04  sed 's/./& /g'

Subshells sind eigene Prozesse, die als Kopien des ursprünglichen Skripts laufen. Das hat Konsequenzen für Variablen. Eine Subshell übernimmt jeden Variablenwert von ihrem Elternprozess. Modifiziert sie diesen Wert, erfahren die Eltern davon nichts, denn die Variable ist ja eine eigenständige Kopie. Endet die Subshell, so gehen all ihre eigenen Variablen verloren. Es gibt konzeptionell keinen Rückweg von Daten aus der Subshell zu ihrem Elternprozess außer ihrem Rückgabewert. Diese Tatsache ist tückisch, weil Subshells nicht nur durch runde Klammern, sondern auch dann entstehen, wenn sie Teil einer Pipe sind [2]. Daher funktioniert auch das Zählen von Zeilen nach diesem Muster nicht:

#!/bin/sh
grep :: /etc/passwd |
while read empty; do
    ((anzahl++))
done
echo "Leere Einträge: $anzahl"

Ab dem While-Statement bis zum Schlüsselwort »done« geschieht alles in einer Subshell, daher ergibt die Auswertung von »$anzahl« nach dem Ende der Schleife auch nur einen leere String.

Zum Debuggen von Subshells bietet sich die Variable »BASH_SUBSHELL« an, die die Verschachtelungsebene anzeigt, so ergibt der Aufruf

( ( (echo $BASH_SUBSHELL) ) )

etwa das Ergebnis 3. Die Variable »SHLVL« ändert sich bei Subshells jedoch nicht. Sie zählt, wie viele echte explizite Shells übereinander laufen.

Vorsicht mit Subshells ist auch bei den Prozess-IDs der Skripte geboten, die Anwender mittels »$$« auslesen. Diese eingebaute Variable gibt nicht etwa die PID der Subshell, sondern die des ablaufenden Skripts als solches aus. Ab der Bash 4 gibt es die eingebaute Variable »BASHPID«, die die tatsächlich im jeweiligen Kontext aktive PID zurückliefert.

Geschweifte Klammern erzeugen übrigens keine Subshell. Sie bilden eine Inline Group, die sich weitgehend wie eine anonyme Funktion ohne Namen verhält. Insbesondere wirken sich Zuweisungen in Inline Groups auch auf die Hauptebene aus. Kuriose Notiz am Rande: Vor der schließenden geschweiften Klammer fordert die Bash einen Strichpunkt, da sie sonst vergeblich nach dem Ende der Inline Group sucht.

Unabhängig von den Eltern

Zusammenfassend lässt sich festhalten, dass Variablen in einer Subshell keine Auswirkungen auf ihre Eltern haben. Genau dieser Effekt unterscheidet sie von Shellfunktionen, wie Listing 2 verdeutlicht: Die zweite Ausgabe in Zeile 11 gibt »elefant« aus, da »func()« den Wert von »demo« ändert.

Listing 2: Scope in
Funktionen

01 #!/bin/sh
02 
03 demo="fant"
04 
05 func() {
06   demo="ele$demo"
07 }
08 
09 echo "$demo"      # ergibt "fant"
10 func
11 echo "$demo"      # ergibt "elefant"

Gerade in Funktionen ist diese Eigenschaft aber eher unbeliebt, muss der Entwickler doch darauf achten, dass er nicht versehentlich eine Variable des Hauptskripts verändert. Wer hier längere, bestehende Skripte mit nützlichen Hilfsfunktionen strukturieren möchte, tappt damit schnell in eine Falle, es sei denn, er benutzt wie Bash-Bashing-Leser Heiko Ettelbrück das Schlüsselwort »local«. Damit begrenzt er nämlich den Scope auf die Funktion und darf es auch nur dort einsetzen.

Auch hier gilt es, Fußangeln auszuweichen: Ettelbrück hatte in den Zeilen 3 bis 13 von Listing 3 eine Funktion geschrieben, um temporäre Dateien für seine Testumgebung aus SAP-Systemen und Applikationsserver anzulegen. Um zu vermeiden, die Variablen »BASE_DIR« und »TMP_DIR« des Hauptprogramms zu überschreiben, deklarierte er sie in der Funktion als »local«. Sollte das Kommando »mktemp« einen Fehler melden, wollte er diesen ab Zeile 7 an die Hauptebene zurückliefern, ansonsten per Stdout den ermittelten Pfad melden.

Listing 3: Fehlerhafte
Funktion

01 #!/bin/bash
02 
03 createTempDir() {
04   local BASE_DIR=$1
05 
06   local TMP_DIR=$(mktemp -d ${BASE_DIR}/XXXX)
07   if [ $? -ne 0 ]; then
08     return 1
09   fi
10 
11   echo "$TMP_DIR"
12   return 0
13 }
14 
15 [...]
16 # Applikation weist DIR ein Verzeichnisnamen zu
17 
18 TMP_DIR=$(createTempDir $DIR)
19 if [ $? -ne 0 ]; then
20   echo "Konnte '$DIR' nicht anlegen."
21   exit 1
22 fi
23 
24 TMP_FILE="${TMP_DIR}/tmpfile"
25 
26 # Kommando schreibt in Temporärdatei:
27 echo "Lege nun $TMP_FILE an ..."
28 Kommando > "$TMP_FILE"
29 
30 [...]

Bash keck? Cash
back!

Seit mehreren Folgen prügelt das "Bash Bashing" auf Unsitten und Missverständlichkeiten der Shell ein - und gibt Fingerzeige, es besser zu machen. Wem daher ein bashenswürdiges Skript vor die Tastatur kommt, schreibt an [redaktion@linux-magazin.de] und kassiert wahlweise 50 Euro Finderlohn oder ein Jahresabo des Linux-Magazins - sollte die Redaktion den Tipp abdrucken. Bei besonders haarsträubenden Fällen sichert die Redaktion selbstredend vertraulichen Informaten- und Opferschutz zu.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 2 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Als digitales Abo

Als PDF im Abo bestellen

comments powered by Disqus

Ausgabe 07/2013

Preis € 6,40

Insecurity Bulletin

Insecurity Bulletin

Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...

Linux-Magazin auf Facebook