Gehupft wie gesprungen
Jede Programmiersprache stellt Sprachelemente bereit, mit denen sich eine Ablaufsteuerung (englisch: ‘flow control’) realisieren lässt. Neben Verzweigungen können Shell-Programmierer Schleifen und rekursive Funktionen einsetzen.
- Michael Riepe
Spaghetti-Code war bereits weit gehend aus der Mode, als Steve R. Bourne in den 70ern seine Unix-Shell schrieb. Befehle wie goto oder jump sucht man daher vergeblich. Statt dessen bietet die Bourne-Shell fast alle Sprachelemente, die man für strukturierte Programmierung benötigt: if und case erlauben ein- und mehrfache Verzweigungen, while und until realisieren ‘abweisende’ Schleifen. Nicht abweisende Schleifen wie repeat ... until in Pascal oder do ... while in C kennt die Shell zwar nicht, sie lassen sich jedoch mit den vorhandenen Mitteln nachbilden.
Eine while-Schleife ist ähnlich aufgebaut wie eine if-Anweisung [1]. Auf das Schlüsselwort folgt ein beliebiger Befehl. Dessen Rückgabewert bestimmt, ob die Shell den ‘Körper’ der Schleife ausführt oder zur nächsten Anweisung springt. Der Schleifenkörper besteht aus einem oder mehreren Befehlen, eingerahmt von den Schlüsselwörtern do und done:
while bedingung
do
befehl
...
doneWie beim if-Befehl steht der Rückgabewert 0 für ‘wahr’, alle übrigen Werte signalisieren ‘falsch’. Ist die Bedingung wahr, führt die Shell den Schleifenkörper aus, springt zurück zum Anfang und wertet die Bedingung erneut aus.
Ersetzt der Programmierer while durch until, führt die Shell die Schleife aus, solange die Bedingung falsch ist. In modernen Abkömmlingen der Bourne-Shell ist until überflüssig: until bedingung lässt sich durch while ! bedingung ersetzen. Ältere Shells kennen jedoch den Negationsoperator ! nicht.
Als Bedingung bietet sich der Befehl test beziehungsweise [ ... ] an. Zeichenketten lassen sich mit test x = y und test x != y vergleichen. test x oder test -n x überprüft, ob x ein oder mehrere Zeichen enthält; test -z x ergibt ‘wahr’, wenn x die leere Zeichenkette ist.
Leider besitzt test ein paar Tücken. Das Kommando test $x = y etwa führt zu einem Syntaxfehler, wenn der Wert der Variablen x nicht aus genau einem Wort besteht. Aus diesem Grund muss der Anwender Ausdrücke mit Variablen in doppelte Anführungszeichen einschließen. Damit nicht genug: Beginnt "$x" mit einem Minuszeichen, interpretiert test es als Option. Erfahrene Shell-Programmierer setzen deshalb ein ‘x’ oder einen anderen Buchstaben vor beide Zeichenketten: Den Ausdruck x’$x’ = xy kann test nicht mehr falsch verstehen.
David Korn spendierte seiner ksh einen neuen Befehl, der einfacher zu handhaben ist: Statt test x’$x’ = xy kann der Benutzer [[ $x = y ]] schreiben. Die Shell analysiert zuerst den Ausdruck und identifiziert Operatoren sowie Argumente. Danach wertet sie die Variablen aus, allerdings ohne ihren Wert in einzelne Wörter zu zerlegen. Daher spielt es keine Rolle, ob eine Variable Leer- oder Minuszeichen enthält.
Außerdem bietet der neue Testbefehl Funktionen, die man bei test vermisst. Er kann zum Beispiel Zeichenketten mit einem Muster vergleichen: [[ $x = *abc* ]] etwa ist wahr, wenn der Wert von x die Zeichenkette ‘abc’ enthält. Leider ist der Befehl nur begrenzt portabel: Neben ksh und ihrer Public-Domain-Schwester pdksh verstehen ihn nur die Z-Shell zsh und Version 2 der Gnu-Shell bash. Dokumentation zum Befehl ist in den Manpages der einzelnen Shells zu finden.
Adam Riese und Eva Shell
Shell-Programme eignen sich gut zur Manipulation von Zeichenketten. Moderne Shells sind außerdem mathematisch begabt: Ganzzahlige Rechenaufgaben lassen sich mit jeder Shell erledigen, die dem Posix-Standard genügt [2].
Für arithmetische Vergleiche in Verzweigungen und Schleifen bieten sich wiederum test und [[ ... ]] an. Mit den sechs binären Operatoren -eq, -ne, -gt, -ge, -lt und -le lassen sich alle Vergleiche durchführen. Der Anwender sollte sich jedoch nicht vom ksh-Befehl [[ $a < $b ]] in die Irre führen lassen: Er vergleicht zwei Zeichenketten miteinander.
Häufig benötigt man in Skripts eine ‘zählende’ Schleife, analog der for-Schleife in Basic oder Pascal. Zwar bietet die Bourne-Shell ebenfalls eine for-Schleife, sie arbeitet jedoch völlig anders: for x in a b c; do echo $x; done etwa gibt nacheinander die Zeichenketten ‘a’, ‘b’ und ‘c’ aus. Eine Schleife, die zum Beispiel von 13 bis 24 zählt, lässt sich mit einem kleinen Kunstgriff erzeugen:
for i in `seq 13 24`
do
...
done
Das Programm seq gibt nacheinander die gewünschten Zahlen aus; die Shell liest sie und fügt sie in den for-Befehl ein. Ist die zweite Zahl kleiner als die erste, zählt seq automatisch rückwärts. Die Shell durchläuft die Schleife wenigstens einmal.
Alternativ kann der Anwender eine Zählschleife aus einer while-Schleife konstruieren:
i=13
while [ $i -le 24 ]
do
...
i=$((i+1))
done
Korn-Shell, Z-Shell und Bash (ab Version 2) bieten einen so genannten ‘arithmetischen Befehl’:
while ((i <= 24))
do
i=$((i+1))
done
Außerdem kennen ksh und bash einen erweiterten for-Befehl, der dem in C ähnelt:
for ((i = 13; i <= 24; i++))
do
...
done
Leider sind beide Varianten nicht portabel. Wer sich an den Posix-Standard halten will oder muss, kann statt des arithmetischen Befehls eine arithmetische Substitution verwenden [2]. Die Hilfsfunktion notzero übersetzt ihr Ergebnis - 1 für wahr, 0 für falsch - in einen für die Shell geeigneten Wahrheitswert (siehe Listing 1).
Listing 1
#! /bin/sh
# Ist $1 != 0?
notzero() {
return $(($1 == 0))
}
# Bewege $1 Scheiben von Stapel $2 nach Stapel $3
# Benutze Stapel $4 als Zwischenablage
hanoi() {
if notzero $(($1 == 1))
then
echo "Von Stapel $2 nach Stapel $3"
else
hanoi $(($1 - 1)) $2 $4 $3
hanoi 1 $2 $3 unbenutzt
hanoi $(($1 - 1)) $4 $3 $2
fi
}
# Hauptprogramm
hanoi $1 1 3 2
Gelegentlich muss ein Programm eine Schleife vorzeitig verlassen. Dazu kann es den eingebauten Befehl break verwenden: Er weist die Shell an, zur nächsten Anweisung hinter der Schleife zu springen. Sein Gegenstück continue bewirkt, dass die Shell zum Anfang der Schleife zurückkehrt und gegebenenfalls einen neuen Durchlauf startet. Beide Befehle funktionieren sowohl in while- als auch in for-Schleifen.
Sind mehrere Schleifen ineinander verschachtelt, lässt sich mit einem zusätzlichen Argument auswählen, welche die Shell abbrechen beziehungsweise wiederholen soll. break oder break 1 beendet die direkt umgebende Schleife, break 2 die zweite von innen und so weiter. continue 2 beendet die innere Schleife und startet die äußere neu.
Schleifen mit einem Ausgang am Ende - so genannte nicht abweisende Schleifen - lassen sich als Endlosschleife programmieren, an deren Ende ein break-Befehl steht:
while true
do
...
bedingung && break
done
Ist die Bedingung wahr, verlässt die Shell die Schleife am Ende des aktuellen Durchlaufs. Schreibt man bedingung || break, endet die Schleife bei einer falschen Bedingung.
Ausführung mit Tiefgang
Manche lieben, andere hassen sie: Die Rede ist von Rekursion, einer Funktion, die sich selbst aufruft, um ein Teil ihres Ergebnisses zu berechnen. Ein klassischer Vertreter ist die Fakultätsfunktion, die das Produkt der Zahlen von 1 bis n berechnet; ihre rekursive Definition lautet n! = n * (n-1)!. Die Abbruchbedingung 1! = 1 verhindert, dass der Selbstaufruf sich bis in die Unendlichkeit fortsetzt. In der Shell wird daraus
fakultaet() {
if notzero $(($1 == 1))
then echo 1
else echo $(($1 * $(fakultaet $(($1 - 1)))))
fi
}
Etwas leserlicher ist die endrekursive Fassung. Doch bedarf es dazu einer weiteren Hilfsfunktion namens fakhilf():
fakhilf() {
if notzero $(($1 == 1))
then echo $2
else fakhilf $(($1 - 1)) $(($1 * $2))
fi
}
fakultaet() {
fakhilf $1 1
}
Soll die Shell fakultaet 4 berechnen, ruft sie zunächst fakhilf 4 1 auf. Die Funktion ruft sich selbst als fakhilf 3 4 auf, dann als fakhilf 2 12 und schließlich als fakhilf 1 24. Im letzten Schritt ist die Abbruchbedingung $1 == 1 erfüllt, die Shell gibt das Ergebnis 24 aus und beendet den rekursiven ‘Abstieg’.
Jede Iteration lässt sich in eine gleichwertige Rekursion umformen und eine Zählschleife als rekursive Funktion schreiben:
von_bis() {
if notzero $(($1 <= $2))
then
...
von_bis $(($1 + 1)) $2
fi
}
von_bis 13 24
Theoretisch käme man ohne Schleifen aus. Allerdings benötigt die Shell in jedem Rekursionsschritt ein Stück Speicher, um ihren aktuellen Zustand zu sichern. Die aktuelle bash hat obendrein die Angewohnheit, einmal angeforderten Speicher nicht wieder freizugeben. Sie kann ihn jedoch ‘recyclen’ und bei Bedarf erneut verwenden.
Manche Aufgaben lassen sich nur schwer ohne Rekursion lösen. Oft leidet vor allem die Lesbarkeit des Programms, wenn der Programmierer einen iterativen Ansatz verwendet. Ein Paradebeispiel sind die ‘Türme von Hanoi’ (siehe Listing 1). Das Skript berechnet rekursiv die Zugfolge für beliebig hohe Stapel; die Stapelhöhe kann der Benutzer als Argument übergeben.
Michael Riepe
studiert Elektrotechnik an der Universität Hannover.
Literatur
[1] Michael Riepe; Gnu-Tips; Unter Kontrolle; Fehlerbehandlung in Shell-Skripts; iX 3/2003, S. 137
[2] Michael Riepe; GNU-Tips; Die Macht des Dollar; Textersetzung in Shell-Kommandos; iX 9/2002, S. 134 (rh)