Nach der Einführung in die Kontrollstrukturen und Vorstellung von einfachen Vergleichsmöglichkeiten im letzten Teil beschäftigen wir uns diesmal mit Reihenvergleichen, Schleifen, Tastatureingaben und kleinen Auswahlmenüs.
In der letzten Ausgabe haben Sie die if-Konstruktion und das Vergleichsprogramm test kennengelernt. Damit war es uns erstmals möglich, den Programmablauf von äußeren Umständen abhängig zu machen; je nach Situation wurden andere Befehle ausgeführt. Die Bash kennt noch weitere Kontrollstrukturen, die insbesondere bei umfangreichen Vergleichen und zum mehrfachen Aufruf einzelner Befehle benötigt werden (Schleifen).
Parametererkennung
Beginnen wollen wir mit einem kleinen Skript. Wie fast jedes andere Linux-Programm soll unser Skript mittels Parameter -h oder --help eine kurze Erklärung der erlaubten Optionen ausgeben und sich dann beenden. Dazu verwenden wir ein if-Konstrukt, wie Sie es im letzten Teil kennen gelernt haben:
#!/bin/bash if [ "$1" = "-h" -o "$1" = "--help" ]; then echo "Aufruf:" echo " $0 [-h|--help]" echo "Parameter:" echo " -h, --help: Kurzerklärung" fi
Relativ umständlich und unübersichtlich wird es, wenn wir mehrere Parameter überprüfen müssen und zudem Groß-/Kleinschreibung ignorieren wollen:
#!/bin/bash
if [ "$1" = "-h" -o "$1" = "-H" -o -z "${1#--[hH][eE][lL][pP]}" ]; then
echo "-h"
elif [ "$1" = "-v" -o "$1" = "-V" ]; then
echo "-v"
elif [ "$1" = "-q" -o "$1" = "-Q" ]; then
echo "-q"
fi
Der Test auf --help in der zweiten Zeile ist erklärungsbedürftig. Um nicht auf --help in allen Varianten der Groß-/Kleinschreibung zu prüfen, verwenden wir die Mustererkennung aus Teil 3 unsers Kurses. Mit ${1#--[hH][eE][lL][pP]} durchsuchen wir die Variable $1 nach einem String, der mit einem Doppel-Minus beginnt und dann ein großes oder kleines H, großes oder kleines E und so weiter enthält. Ist in $1 tatsächlich --help in einer beliebigen Groß-/Klein-Kombination enthalten, wird es entfernt – übrig bleibt eine leere Zeichenkette. Hier kommt der Test-Parameter -z ins Spiel: Er liefert dann einen wahren Wert, wenn die dahinter stehende Zeichenkette leer ist – also eine Version von --help gefunden wurde. Worte, die nur mit --help beginnen (zum Beispiel --helper) fallen beim Test durch, weil die Endung übrig bleiben würde.
Vereinfachung mit <C>case<C>
Wie am vorhergehenden Beispiel zu sehen: Umfangreiche Parameter-Prüfungen kann man so kaum durchführen. Vereinfachung tut Not. Das Vorgehen beim Parameter-Vergleich ist stets gleich: Wir prüfen in jedem Fall, ob der erste Parameter eine bestimmte Gegebenheit erfüllt. Für solche Reihenvergleiche gibt es die case-Konstruktion:
#!/bin/bash
case $1 in
-h|-H|--[hH][eE][lL][pP])
echo "-h"
;;
-v|-V)
echo "-v"
;;
-q|-Q)
echo "-q"
;;
esac
Das case-Konstrukt besteht aus den umschließenden Schlüsselworten case und esac (wie das schließende fi zu if ist auch esac die Umkehrung der Buchstaben aus case), einer Zeichenkette (hier $1), die überprüft wird, und den einzelnen Blöcken mit den jeweiligen Fällen. Diese Blöcke beginnen mit dem Muster, das von einer schließenden runden Klammer begrenzt wird, und enden mit einem Doppel-Semikolon – dazwischen stehen die Anweisungen, die für den jeweiligen Fall ausgeführt werden sollen.
In unserem Beispiel haben wir drei unterschiedliche Fälle, -h, -v und -q. Das Muster des ersten Falls besteht aus drei Teilen, von denen eines wahr sein muss. Die Muster selbst sind nahezu identisch mit denen aus unserem if-Konstrukt, allerdings deutlich übersichtlicher.
Neben den eckigen Klammern, mit denen man erlaubte Zeichen oder Zeichenbereiche angeben kann, gibt es noch die Platzhalter (Wildcards) ? für ein beliebiges Zeichen und * für beliebige Zeichenfolgen. Damit lassen sich zum Beispiel die verschiedenen Netzwerk-Devices voneinander unterscheiden:
case $device in
eth*)
echo "Ethernet"
;;
ppp*)
echo "Modem"
;;
ippp*)
echo "ISDN"
;;
lo)
echo "Loopback"
;;
*)
echo "Unbekannt"
esac
Das letzte Muster, “*”, trifft auf jede beliebige Zeichenkette zu – weshalb dieses case-Konstrukt eigentlich stets “Unbekannt” liefern müsste. Doch die Fälle werden von oben nach unten abgearbeitet, und es wird nur der erste ausgeführt, der passt: Alle weiteren werden ignoriert. So wird bei “ppp0” nur “Modem” ausgegeben, nicht aber “Unbekannt”. Das Skript wird dann nach Abarbeitung des Falls hinter dem esac-Schlüsselwort fortgesetzt.
Schleifen
Mit Schleifen ist es möglich, Programmteile mehrfach ausführen zu lassen, zum Beispiel, um alle Kommandozeilen-Parameter nacheinander auszuwerten. Die Bash kennt dreierlei Scheifen-Konstrukte: for, while und until. Prinzipiell sind die drei Schleifen gegeneinander austauschbar: Was man mit for löst, kann in jedem Fall auch mit while geschrieben werden. Dennoch sollte man sich überlegen, welches Schleifenkonstrukt zu welchem Problem am besten passt.
for
Die for-Schleife eignet sich für Anwendungen, bei denen eine Liste von Variablen feststeht und einzeln verarbeitet werden muss. Sie ist zum Beispiel praktisch, um mit unserem case-Konstrukt die Kommandozeilen-Parameter auszuwerten:
#!/bin/bash
for P in $@; do
case $P in
-h|-H|--[hH][eE][lL][pP])
echo "-h"
;;
-v|-V)
echo "-v"
;;
-q|-Q)
echo "-q"
;;
*)
echo "Unerlaubter Parameter $P"
;;
esac
done
$@ liefert eine Liste aller Kommandozeilen-Parameter, die for dann nacheinander in die Variable P einträgt, um dann den Schleifenrumpf mit unserem case-Konstrukt auszuführen.
Wer bereits andere Programmiersprachen kennt, wird von der Arbeitsweise der for-Schleife etwas überrascht sein, in Perl arbeitet sie zum Beispiel ganz anders: Man gibt meist einen Start- und einen Endwert an, zudem die Schrittweite, in der der Startwert erhöht wird. Die Schleife wird dann so lange abgearbeitet, bis der Endwert erreicht ist – was zum Beispiel dafür benutzt wird, um die Werte 1 bis 10 eines Feldes einzulesen.
In der Bash ist dies nicht direkt vorgesehen, doch können wir uns mit dem Hilfsprogramm seq aus dem Paket sh_utils (oder sh-utils) behelfen. seq liefert uns eine Zahlensequenz von einem angegebenen Startwert bis zu einem Endwert, optional kann auch die Schrittweite eingestellt und zudem das Zahlenformat noch geändert werden. Um zum Beispiel zehn mal “Hallo Welt” auszugeben, könnten wir folgendes Skript verwenden, wobei die Nummer des jeweiligen Durchlaufs in eckigen Klammern voran gestellt wird:
#!/bin/bash for i in `seq 1 10`; do echo "[$i] Hallo Welt" done
Leider gibt es keine Manual Page zu seq, eine relativ ausführliche Hilfe erhalten Sie aber über den Parameter --help. Für den Hausgebrauch reichen die Aufrufe mittels seq Start Ende und seq Start Schrittweite Ende, um die meisten Fälle abzudecken.
while
Die wohl am häufigsten verwendete Schleifenkonstruktion ist while. Hier wird der Schleifenrumpf so lange ausgeführt, wie die angegebene Bedingung wahr ist. Eine mögliche Anwendung ist das Einlesen eines beliebig langen Feldes:
#!/bin/bash
i=1
while
read -e -p "[$i]> ";
do
Feld[$i]="$REPLY"
: $[i+=1]
done
echo ${Feld[*]}
Die Bedingung ist in diesem Fall ein Aufruf von read, es kann aber genau wie beim if-Konstrukt test oder jedes andere Programm verwendet werden. Ist der Rückgabewert des Programms 0, ist die Bedingung wahr, andernfalls falsch.
Neu ist das read-Kommando, mit dem wir erstmals Eingaben von der Tastatur (genauer Standardeingabe) lesen. Die Eingabe wird – so nichts anderes angegeben ist – in der Variablen REPLY gespeichert. Der Parameter -e aktiviert die ReadLine-Erweiterung, damit können Sie die Eingabezeile wie gewohnt mit den Cursor-Tasten bearbeiten, auch die Tab-Completion von Programmnamen funktioniert. Der zweite Parameter -p "[$i]> " bestimmt den Prompt, der am Anfang der Eingabezeile angezeigt wird. In diesem Fall ist das die Elementnummer im Array, $i in eckigen Klammern mit einem Kleiner-Zeichen und Leerzeichen dahinter. Anders als bei echo wird kein Zeilenumbruch an den Prompt angehängt, der Cursor bleibt dahinter stehen.
Der Rest des Skripts ist schnell erklärt, while prüft jedesmal, ob read wahr ist – was genau dann eintritt, wenn etwas eingegeben wurde. Die Eingabe hat read in die Variable REPLY gelegt, deren Inhalt wir im Schleifenrumpf als Element Nummer $i unseres Arrays Feld speichern. Anschließend wird i erhöht, um nicht die gespeicherten Werte zu überschreiben, und die Schleife beginnt von neuem.
Erst wenn statt eines Wertes [Strg-D] gedrückt wird, ist der Rückgabewert von read ungleich 0, die Bedingung also nicht erfüllt. Die Schleife endet, und die nächste Anweisung hinter dem zur Schleife gehörigen done wird ausgeführt. In unserem Fall ist es die Ausgabe aller Elemente unseres Arrays Feld, was wir durch Angabe eines Sterns an Stelle der Element-Nummer erreichen.
Programmvereinfachung
Das gerade gezeigte Beispiel ist noch recht ausführlich und sogar relativ umständlich geschrieben. Es geht deutlich kürzer und schneller:
#!/bin/bash
while
read -e -p "[$[i+=1]]> " Feld[$i];
do
:
done
echo ${Feld[*]}
Das auf den ersten Blick ungewöhnlichste ist der leere Schleifenrumpf, er enthält nur den Doppelpunkt als Null-Funktion. Dieser ist allerdings notwendig, denn der Rumpf darf nicht leer sein. Die Umspeicherung von REPLY in das Feld-Element entfällt, durch Angabe des Variablen-Namens Feld[$i] als letzten Parameter von read werden die eingegebenen Werte sofort im Array und nicht erst in REPLY abgelegt. Auch das Erhöhen der Variablen i um 1 ist in die Bedingung verlegt und zudem die Initialisierung mit 1 entfernt worden. Der Clou ist, dass neue Variablen standardmäßig leer sind, aber den arithmetischen Wert 0 haben. Die Anweisung $[i+=1] erhöht erst die Variable um 1 und liefert dann den Wert. Somit beginnen wir nach wie vor bei Element Nummer 1.
Tabelle 1: <C>read<C>-Parameter
-a Feld |
Elaubt die Eingabe mehrerer durch Leerzeichen getrennter Werte. Die Werte werden im Array Feld von Element 0 an aufsteigend gespeichert. |
-e |
Aktiviert die ReadLine-Unterstützung. Damit wird es möglich, die Eingabezeile zum Beispiel mit Cursor-Tasten oder Tab-Completion zu bearbeiten. |
-r |
Deaktiviert die Backslash-Enter-Sonderbehandlung. Normalerweise ist es möglich, eine Eingabe durch einen Backslash am Ende einer Zeile in der nächsten Zeile weiterzuführen, ohne dass der Zeilenumbruch eine Auswirkung hat. |
-p Prompt |
Ersetzt die Standard-Eingabe-Aufforderung durch die Zeichenkette Prompt, ohne einen Zeilenumbruch anzuhängen. |
| Name | Speichert die Eingabe direkt in der Variablen Name und nicht wie üblich in REPLY. |
until
until tut übrigens das gleiche wie while, nur dass der Rumpf ausgeführt wird, solange die Bedingung falsch ist – sonst besteht kein Unterschied. Das folgende Beispiel kennen Sie von der Vorstellung der while-Schleife, die Bedingung wurde mittels Ausrufezeichen invertiert – aus wahr wird falsch und umgekehrt.
#!/bin/bash
i=1
until
! read -e -p "[$i]> ";
do
Feld[$i]="$REPLY"
: $[i+=1]
done
echo ${Feld[*]}
In Listing 1 finden Sie den Rohentwurf eines Skripts, das neben -h und --help noch die Parameter -q respektive --quiet sowie -v gleichbedeutend mit --verbose versteht. -q und -v werden häufig verwendet, um entweder alle Ausgaben bis auf schwerwiegende Fehlermeldungen zu unterdrücken beziehungsweise alle Aktionen zu kommentieren. Standardmäßig wird der ausführliche Modus über die Variable QuietMode eingestellt, indem diese in der zweiten Zeile auf 1 gesetzt wird. Auch wenn es im ersten Moment widersinnig erscheint: Da 0 wahr ist, setzen wir QuietMode mit 1 auf falsch.
Listing 1
#!/bin/bash
QuietMode=1
for P in $@; do
case $P in
-h|-H|--[hH][eE][lL][pP])
echo "Aufruf:"
echo " $0 [-h|--help]|[-q|--quiet]|[-v|--verbose]"
echo "Parameter:"
echo " -h, --help: Diese Kurzerläuterung"
echo " -q, --quiet: Nur schwerwiegende Fehler melden"
echo " -v, --verbose: Ausführliche Meldungen"
exit
;;
-v|-V|--[vV][eE][rR][bB][oO][sS][eE])
QuietMode=1
;;
-q|-Q|--[qQ][uU][iI][eE][tT])
QuietMode=0
;;
*)
echo "Fehler: Unbekannter Parameter $i"
exit
;;
esac
done
echo $QuietMode
Auswahlmenüs
Eine häufig unterschätzte Möglichkeit bietet die select-Konstruktion. Mit ihr wird es möglich, komplexe Auswahlmenüs aufzubauen. Hier ein einfaches Beispiel für eine Auswahl zwischen Äpfeln, Birnen und Pflaumen:
#!/bin/bash
Feld=("Äpfel" "Birnen" "Pflaumen" "Ende")
select Fruechte in ${Feld[*]}; do
case $REPLY in
${#Feld[*]})
return
;;
*)
echo "$Fruechte"
;;
esac
done
Hinter ${Feld[*]} verbirgt sich der Inhalt unseres Arrays, in diesem Fall vier Elemente, die select durchnumeriert und unter einander ausgibt:
1) Äpfel 2) Birnen 3) Pflaumen 4) Ende #?
Jetzt fragt select die Nummer der gewünschten Aktion ab, wozu implizit read verwendet wird. Das Ergebnis der Auswahl steht wie üblich später in der Variablen REPLY zur Verfügung. Zusätzlich kopiert select das entsprechende Element unseres Arrays in die Variable P und führt den Rumpf aus. Ist dieser abgearbeitet, zeigt select die Auswahl von neuem.
Im Rumpf wird mittels case das weitere Vorgehen bestimmt. Das erste Muster sieht zunächst etwas ungewöhnlich aus, hinter ${#Feld[*]} verbirgt sich aber nur die Anzahl der enthaltenen Elemente – was gleichbedeutend ist mit der Nummer des letzten Eintrags. Damit können wir zuverlässig erkennen, wenn der Anwender den letzten Eintrag ausgewählt hat, ohne dessen tatsächliche Nummer oder Namen kennen zu müssen – die Auswahl ist also beliebig erweiterbar, solange der letzte Eintrag für “Verlassen des Menüs” steht.
Um select zu verlassen, müssen Sie entweder [Strg-D] drücken oder wie im Beispiel gezeigt return oder break aufrufen. Insofern unterscheidet sich select auch von allen anderen Schleifen – es hat weder eine Bedingung noch eine Liste, nach deren Abarbeitung die Schleife endet.
Anders als bei read können Sie für select nicht den auszugebenden Prompt als Parameter mitliefern – es kommt der Standard-Rückfrage-Prompt aus der Variablen PS3 zum Einsatz, den Sie von Hand anpassen können:
#!/bin/bash
Feld=("Äpfel" "Birnen" "Pflaumen" "Ende")
PS3="Welche Früchte? > "
select Fruechte in ${Feld[*]}; do
case $REPLY in
${#Feld[*]})
return
;;
*)
echo "$Fruechte"
;;
esac
done
Damit endet der fünfte Teil des Programming Corners. In Teil 6 wird es um die Strukturierung und Modularisierung von Skripten mittels Funktionen und Modulen gehen. Anhand eines kleinen Verwaltungsprogramms werden wir dann in Teil 7 noch einmal alles Bisherige aufrollen und die Anwendungsmöglicheiten der einzelnen Befehle und Konstrukte zeigen.
