Zum Inhalt springen

Kontrollstrukturen

Ein Skript, das immer dasselbe tut, ist eine Befehlsliste. Interessant wird es, wenn das Skript auf Eingaben reagiert, Entscheidungen trifft und Dinge wiederholt. Dafür gibt es Kontrollstrukturen. Du lernst sie an einem Menü-Skript kennen, das am Ende dein addscript.sh bedient.

Das zahlt sich jetzt aus: Das Gerüst des neuen Skripts erzeugst du mit addscript.sh aus Teil 1.

Terminal-Fenster
cd labs/linux/08-skripte
./addscript.sh menu.sh "Interaktives Menü"

Der Editor öffnet sich mit dem fertigen Kopf. Ergänze das Hauptprogramm um die Ausgabe des Menüs:

menu.sh
#!/bin/bash
#
# menu.sh
# Interaktives Menü
#
# 2026-11-05 / Vorname Nachname
#
# Variablen
# Funktionen
# Hauptprogramm
# Menü ausgeben
echo "
==== Skript-Menü ====
1 Alle Shell-Skripte auflisten
2 Neues Shell-Skript erstellen
e Ende
"

Testen. ./menu.sh sollte das Menü anzeigen. Mehr noch nicht.

Das Menü ist bisher reine Dekoration. Eingaben liest read und legt sie in einer Variablen ab. Du kennst den Befehl aus Aufgabe 8.1:

menu.sh
# Menü ausgeben
echo "
==== Skript-Menü ====
1 Alle Shell-Skripte auflisten
2 Neues Shell-Skript erstellen
e Ende
"
# Eingabe einlesen
read -r -p "Auswahl eingeben, mit Enter bestätigen: " antwort
echo "Du hast '$antwort' gewählt."

-p gibt die Eingabeaufforderung aus und lässt den Cursor dahinter stehen, -r verhindert, dass Backslashes in der Eingabe umgedeutet werden. Die letzte Zeile ist eine Kontrollausgabe. Testen: Gib eine Zahl ein, einen Buchstaben, nichts, einen Satz mit Leerzeichen.

Nach einer Eingabe ist das Skript fertig. Ein Menü soll aber so lange anzeigen und fragen, bis die Person es beendet. Das ist eine Schleife mit Abbruchbedingung:

flowchart TD
    S(["Start"]) --> M["Menü ausgeben"]
    M --> R["Eingabe einlesen"]
    R --> Q{"Eingabe = e?"}
    Q -->|nein| M
    Q -->|ja| E(["Ende"])

Die while-Schleife wiederholt ihren Block, solange die Bedingung wahr ist:

Terminal-Fenster
while bedingung; do
befehle # werden wiederholt, solange die Bedingung wahr ist
done

Für ein Menü nutzt man eine spezielle Variante: true ist ein Befehl, der immer Erfolg meldet, also läuft while true endlos. Verlassen wird die Schleife von innen mit break.

menu.sh
# Hauptprogramm
# Endlosschleife, verlassen mit break
while true; do
# Menü ausgeben
echo "
==== Skript-Menü ====
1 Alle Shell-Skripte auflisten
2 Neues Shell-Skript erstellen
e Ende
"
# Eingabe einlesen
read -r -p "Auswahl eingeben, mit Enter bestätigen: " antwort
echo "Du hast '$antwort' gewählt."
# Auswahl e beendet das Menü
if [[ $antwort == "e" ]]; then
break
fi
done

Der ganze bisherige Block wandert in die Schleife und wird eingerückt (die Zeilen des Menütexts nicht, sie würden sonst eingerückt ausgegeben). Testen: Beendet e das Skript? Was passiert bei E? Falls du dich verrannt hast: Ctrl+C bricht jedes Skript ab.

Für drei Menüpunkte bräuchte es drei if-Blöcke, für zehn Punkte zehn. Das wird unübersichtlich. Für die Auswahl aus mehreren Werten gibt es case:

Terminal-Fenster
case "$variable" in
wert1)
befehle # bei wert1
;;
wert2|wert3)
befehle # bei wert2 oder wert3
;;
*)
befehle # bei allem anderen
;;
esac

Die Bash vergleicht den Wert der Reihe nach mit jedem Muster und führt den ersten passenden Block aus. ;; schliesst einen Block ab. Die Muster sind dieselben wie bei Dateinamen: [eE] steht für e oder E, | trennt Alternativen, * passt auf alles und fängt am Schluss den Rest ab.

flowchart TD
    M["Menü ausgeben"] --> R["Eingabe einlesen"]
    R --> C1{"Eingabe = 1?"}
    C1 -->|ja| A1["Aktion 1"] --> M
    C1 -->|nein| C2{"Eingabe = 2?"}
    C2 -->|ja| A2["Aktion 2"] --> M
    C2 -->|nein| C3{"Eingabe = e oder q?"}
    C3 -->|ja| E(["Ende"])
    C3 -->|nein| U["Ungültige Eingabe melden"] --> M

Die if-Verzweigung wird durch ein case ersetzt. Das Menü lässt sich neu mit e, E, q oder Q beenden:

menu.sh
# Eingabe einlesen
read -r -p "Auswahl eingeben, mit Enter bestätigen: " antwort
# Auswahl e beendet das Menü
if [[ $antwort == "e" ]]; then
break
fi
# Je nach Eingabe handeln
case "$antwort" in
1)
echo "=> Menüpunkt $antwort gewählt"
# hier kommt später die Aktion hin
;;
2)
echo "=> Menüpunkt $antwort gewählt"
# hier kommt später die Aktion hin
;;
[eE]|[qQ])
echo "=> Ende"
break
;;
*)
echo "=> Ungültige Eingabe: '$antwort'"
;;
esac
done

Testen. Verhält sich das Skript wie im Flowchart? Wenn du den Ablauf nicht erklären kannst, ist jetzt der Moment, ihn mit bash -x ./menu.sh zu beobachten: Die Bash zeigt dann jede Zeile, bevor sie sie ausführt.

Der Menütext ist fix im echo verdrahtet. Ein neuer Menüpunkt müsste dort und im case nachgetragen werden. Besser: Die Einträge liegen in einem Array, einer Variablen mit mehreren Werten, und das Menü wird daraus erzeugt.

menu.sh
# Variablen
titel="==== Skript-Menü ===="
menu=(
"Alle Shell-Skripte auflisten"
"Neues Shell-Skript erstellen"
"Ende"
)
anzahl=${#menu[@]}

Die Werte stehen in runden Klammern, durch Leerzeichen oder Zeilenumbrüche getrennt. Die Nummerierung beginnt bei 0: ${menu[0]} ist der erste Eintrag, ${menu[2]} der dritte. ${#menu[@]} liefert die Anzahl Elemente, "${menu[@]}" alle Elemente einzeln. Die geschweiften Klammern sind bei Arrays Pflicht, ohne sie würde die Bash $menu[0] als Variable menu gefolgt vom Text [0] lesen.

Für die Ausgabe eignet sich die Zählschleife im C-Stil: for ((start; bedingung; schritt)). Die Variable i beginnt bei 0, die Schleife läuft, solange i kleiner als anzahl ist, und i++ erhöht i nach jedem Durchgang um 1. Weil das Array bei 0 beginnt, werden auch die Menüpunkte ab 0 nummeriert, und das case zieht nach.

Zur Kontrolle das vollständige Skript:

menu.sh
#!/bin/bash
#
# menu.sh
# Interaktives Menü
#
# 2026-11-05 / Vorname Nachname
#
# Variablen
titel="==== Skript-Menü ===="
menu=(
"Alle Shell-Skripte auflisten"
"Neues Shell-Skript erstellen"
"Ende"
)
anzahl=${#menu[@]}
# Funktionen
# Hauptprogramm
# Endlosschleife, verlassen mit break
while true; do
# Menü ausgeben
echo
echo "$titel"
for ((i = 0; i < anzahl; i++)); do
echo " $i) ${menu[$i]}"
done
# Eingabe einlesen
read -r -p "Auswahl eingeben, mit Enter bestätigen: " antwort
# Je nach Eingabe handeln
case "$antwort" in
0)
echo "=> ${menu[0]}"
# hier kommt später die Aktion hin
;;
1)
echo "=> ${menu[1]}"
# hier kommt später die Aktion hin
;;
2|[eE]|[qQ])
echo "=> ${menu[2]}"
break
;;
*)
echo "=> Ungültige Eingabe: '$antwort'"
;;
esac
done

Testen. Füge probehalber einen vierten Eintrag ins Array ein. Was ändert sich im Menü, was im Verhalten?

for läuft auch direkt über eine Liste von Wörtern, über Dateinamen (Glob) oder über alle Argumente des Skripts:

Terminal-Fenster
for frucht in Apfel Banane Mango; do
echo "Frucht: $frucht"
done
for datei in ./*.sh; do
echo "Skript: $datei"
done
for argument in "$@"; do
echo "Argument: $argument"
done

until ist das Gegenstück zu while: Der Block läuft, bis die Bedingung wahr wird. Für Zähler nimmst du (( )), die arithmetische Auswertung der Bash:

Terminal-Fenster
i=1
until (( i > 3 )); do
echo "Durchgang $i"
(( i += 1 ))
done

Neben nummerierten gibt es Arrays mit frei wählbaren Schlüsseln, die mit declare -A angelegt werden:

Terminal-Fenster
declare -A hauptstadt=( [Schweiz]="Bern" [Italien]="Rom" )
echo "${hauptstadt[Schweiz]}"
for land in "${!hauptstadt[@]}"; do
echo "$land: ${hauptstadt[$land]}"
done

"${!hauptstadt[@]}" mit Ausrufezeichen liefert die Schlüssel, ohne Ausrufezeichen die Werte. Die Reihenfolge ist nicht festgelegt. Beide Array-Arten führt scripts/linux/08-skripte/arraydemo.sh vor.

Was fehlt, sind die Aktionen. Punkt 0 listet die Skripte, Punkt 1 ruft addscript.sh auf:

menu.sh
case "$antwort" in
0)
echo "=> ${menu[0]}"
ls -l ./*.sh
;;
1)
echo "=> ${menu[1]}"
./addscript.sh einneuesskript.sh "Test aus menu.sh"
;;

Warum ./*.sh und nicht *.sh? Eine Datei namens -l.sh würde sonst als Option an ls gehen. ShellCheck meldet das als SC2035.

Testen. Wähle 0, dann 1, dann nochmals 1. Der zweite Aufruf scheitert, weil die Datei schon existiert. Das Skript ist so sinnfrei, es könnte nur ein einziges Mal ein Skript anlegen. Das zu beheben ist deine Aufgabe.

Lösung anzeigen

8.3: Menüpunkt 1 fragt beide Werte ab. Damit menu.sh sein addscript.sh auch findet, wenn es von einem anderen Verzeichnis aus gestartet wird, nimmt die Lösung den Pfad aus $0: dirname "$0" liefert das Verzeichnis, in dem menu.sh liegt.

menu.sh
# Variablen
titel="==== Skript-Menü ===="
menu=(
"Alle Shell-Skripte auflisten"
"Neues Shell-Skript erstellen"
"Ende"
)
anzahl=${#menu[@]}
skriptpfad=$(dirname "$0") # Verzeichnis, in dem menu.sh liegt
# Hauptprogramm
# ...
case "$antwort" in
0)
echo "=> ${menu[0]}"
ls -l ./*.sh
;;
1)
echo "=> ${menu[1]}"
read -r -p "Name des Skripts: " skriptname
read -r -p "Zweck des Skripts: " skriptzweck
"$skriptpfad/addscript.sh" "$skriptname" "$skriptzweck"
;;

Bei leerem Namen meldet addscript.sh den Fehler No such file or directory, weil die Argumentprüfung nur zählt, nicht prüft, ob $1 leer ist. Ein leeres Argument "" zählt. Ein [[ -z "$skriptname" ]] vor dem Aufruf fängt das ab.

8.4: Siehe scripts/linux/08-skripte/planeten.sh. Der Kern:

planeten.sh
read -r -p "Welcher Planet interessiert dich? " planet
case "$planet" in
[mM]erkur) echo "Merkur: 1. Planet, Gesteinsplanet." ;;
[vV]enus) echo "Venus: 2. Planet, Gesteinsplanet." ;;
[eE]rde) echo "Erde: 3. Planet, Gesteinsplanet." ;;
[mM]ars) echo "Mars: 4. Planet, Gesteinsplanet." ;;
[jJ]upiter) echo "Jupiter: 5. Planet, Gasriese." ;;
[sS]aturn) echo "Saturn: 6. Planet, Gasriese." ;;
[uU]ranus) echo "Uranus: 7. Planet, Eisriese." ;;
[nN]eptun) echo "Neptun: 8. Planet, Eisriese." ;;
"")
echo "Keine Eingabe, kein Planet." >&2
exit 1
;;
*)
echo "'$planet' kenne ich nicht." >&2
exit 1
;;
esac

8.5: Ausgabe sind drei Zeilen (Apfel, Banane, Mango), dann «Kiwi, Allergie!» und Abbruch mit Status 1. Die Melone kommt nie an die Reihe.

fruchtsalat.sh
#!/bin/bash
#
# fruchtsalat.sh
# Gibt die Früchte der Liste aus, bricht bei Kiwi ab.
#
# 2026-11-05 / Vorname Nachname
#
# Variablen
fruechte=( Apfel Banane Mango Kiwi Melone )
# Hauptprogramm
for frucht in "${fruechte[@]}"; do
if [[ $frucht == "Kiwi" ]]; then
echo "Kiwi, Allergie!"
exit 1
fi
echo "Aktuelle Frucht: $frucht"
done

Selbstcheck · 6 Fragen

1. Wie wird eine Schleife mit while true verlassen?
2. Was bewirkt das Muster *) in einem case?
3. Welche case-Muster passen auf die Eingabe Q? (mehrere Antworten möglich)
4. Was liefert ${#menu[@]}?
5. Was bewirkt die Option -r bei read?
6. Mit welchem Index beginnt ein Bash-Array?