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)