Fortgeschrittene Muster, Fallthrough und case als Ersatz für lange if/elif-Ketten
Das case-Statement wird in vielen Bash-Skripten nur für eine Handvoll fester Zeichenketten genutzt, kann aber deutlich mehr: mehrere Pattern in einem Zweig, Wildcard-Kombinationen, gezieltes Fallthrough mit ;& und ;;& sowie case-insensitive Matching. Wer diese Werkzeuge kennt, ersetzt lange, schwer wartbare if/elif-Ketten durch kompakte, gut lesbare Verzweigungen.
Inhaltsverzeichnis
- 1. Grundstruktur des case-Statements und warum es mehr kann als drei Optionen
- 2. Mehrere Pattern pro Zweig mit dem Alternations-Operator |
- 3. Wildcard-Kombinationen: Zeichenklassen, Präfixe und Suffixe
- 4. Unbedingtes Fallthrough mit ;& in Bash 4 und neuer
- 5. Bedingtes Weiterprüfen mit ;;& seit Bash 4
- 6. case für Argument-Parsing: extglob-Muster in der Praxis
- 7. Case-insensitive Matching mit shopt -s nocasematch
- 8. Quoting-Fallstricke: wann ein Muster literal und wann als Glob gilt
- 9. case als Alternative zu langen if/elif-Ketten
- 10. Zusammenfassung
- 11. FAQ
1. Grundstruktur des case-Statements und warum es mehr kann als drei Optionen
Ein case-Statement vergleicht einen einzelnen Ausdruck gegen eine Liste von Mustern und führt den Codeblock des ersten passenden Musters aus, abgeschlossen mit esac. Der entscheidende Unterschied zu einer Kette aus if/elif ist, dass der zu prüfende Ausdruck nur ein einziges Mal ausgewertet wird, während eine if/elif-Kette bei jeder Bedingung erneut denselben Ausdruck oder Befehl aufrufen kann, was bei rechenintensiven Prüfungen oder Befehlssubstitutionen einen echten Unterschied macht.
Viele Skripte nutzen case ausschließlich für einfache, feste Zeichenketten wie start, stop oder restart, lassen dabei aber die eigentliche Stärke des Konstrukts ungenutzt: Jedes Pattern ist ein vollwertiges Glob-Muster mit Wildcards, Zeichenklassen, Alternativen durch | und, sobald extglob aktiv ist, sogar erweiterten Musteroperatoren wie @(...) und !(...).
#!/usr/bin/env bash
set -euo pipefail
read -r command
case "$command" in
start)
echo "Starting service"
;;
stop)
echo "Stopping service"
;;
*)
echo "Unknown command: $command" >&2
exit 1
;;
esac
2. Mehrere Pattern pro Zweig mit dem Alternations-Operator |
Innerhalb eines einzelnen Zweigs lassen sich mehrere Muster mit dem Pipe-Zeichen | kombinieren, sodass jedes der aufgelisteten Muster denselben Codeblock auslöst. Das ist besonders nützlich, um mehrere Schreibweisen oder Abkürzungen desselben Befehls zusammenzufassen, etwa -h und --help, oder um Groß- und Kleinschreibvarianten abzudecken, ohne für jede einzelne einen eigenen, inhaltlich identischen Zweig zu schreiben.
Wichtig ist, dass die Muster innerhalb eines |-getrennten Zweigs unabhängig voneinander geprüft werden und die Reihenfolge innerhalb der Liste keine Rolle spielt, solange keines der Muster mit einem früheren Zweig kollidiert. Da case immer nur den ersten passenden Zweig ausführt, sollten spezifischere Muster grundsätzlich vor allgemeineren Wildcard-Mustern stehen, sonst wird der spezifische Zweig nie erreicht.
#!/usr/bin/env bash
set -euo pipefail
read -r flag
case "$flag" in
-h|--help|-\?)
echo "Showing help text"
;;
-v|--version)
echo "Showing version"
;;
yes|y|Y|YES)
echo "Confirmed"
;;
*)
echo "Unrecognized flag: $flag" >&2
exit 1
;;
esac
3. Wildcard-Kombinationen: Zeichenklassen, Präfixe und Suffixe
Weil jedes case-Pattern ein normales Glob-Muster ist, funktionieren dieselben Wildcards wie beim Dateiglobbing: * für eine beliebige Zeichenfolge, ? für genau ein Zeichen und [abc] beziehungsweise [0-9] für Zeichenklassen. Ein Muster wie *.log matcht jede Zeichenkette, die auf .log endet, während [0-9][0-9] genau zwei aufeinanderfolgende Ziffern verlangt, was sich gut für einfache Format-Validierungen eignet, etwa das Prüfen eines zweistelligen Monats.
Kombinierte Wildcard-Muster sind besonders bei der Klassifizierung von Eingabewerten hilfreich, etwa um zwischen verschiedenen Versionsformaten, Log-Level-Präfixen oder Dateinamensschemata zu unterscheiden. Ein häufiger Anwendungsfall ist die Erkennung von IP-Adressbereichen oder Versionsnummern-Präfixen direkt im case-Muster, ohne dafür einen separaten regulären Ausdruck mit grep oder [[ =~ ]] aufrufen zu müssen.
#!/usr/bin/env bash
set -euo pipefail
read -r log_line
case "$log_line" in
ERROR:*|FATAL:*)
echo "Critical: $log_line"
;;
WARN:*)
echo "Warning: $log_line"
;;
[0-9][0-9][0-9]\ *)
echo "Numeric status code line: $log_line"
;;
*)
echo "Info: $log_line"
;;
esac
4. Unbedingtes Fallthrough mit ;& in Bash 4 und neuer
Standardmäßig beendet case die Verarbeitung nach dem ersten passenden Zweig vollständig, ohne die folgenden Zweige zu betrachten, vergleichbar mit einem impliziten break in anderen Sprachen. Ab Bash 4 steht mit ;& statt des üblichen ;; ein Terminator zur Verfügung, der nach der Ausführung des aktuellen Zweigs bedingungslos auch den Code des direkt nächsten Zweigs ausführt, ohne dessen Pattern überhaupt zu prüfen.
Dieses Verhalten eignet sich für gestufte Aktionen, bei denen eine höhere Stufe automatisch alle Aktionen der niedrigeren Stufen mit einschließen soll, etwa ein Log-Level-System, bei dem debug auch alle info-Ausgaben mit anzeigen soll. Wichtig ist, ;& bewusst und sparsam einzusetzen, weil es die lineare Lesereihenfolge des Skripts durchbricht und ein Leser, der nur den aktuellen Zweig liest, das Fallthrough-Verhalten leicht übersieht.
#!/usr/bin/env bash
set -euo pipefail
readonly LEVEL="${1:-info}"
case "$LEVEL" in
debug)
echo "[DEBUG] verbose diagnostic output"
;&
info)
echo "[INFO] general status messages"
;;
*)
echo "Unknown level: $LEVEL" >&2
exit 1
;;
esac
5. Bedingtes Weiterprüfen mit ;;& seit Bash 4
Neben ;& gibt es seit Bash 4 zusätzlich ;;&, das sich subtil, aber wichtig unterscheidet: Statt den nächsten Zweig bedingungslos auszuführen, prüft Bash nach ;;& die nachfolgenden Muster weiterhin regulär und führt nur den Codeblock jedes tatsächlich passenden Zweigs aus. Das erlaubt es, mehrere unabhängige Prüfungen gegen denselben Wert durchzuführen, ohne den Wert in mehreren separaten case-Blöcken erneut auszuwerten.
Ein praktisches Beispiel ist die Validierung eines Dateinamens gegen mehrere unabhängige Kriterien gleichzeitig, etwa Dateiendung und Namenskonvention, wobei jedes zutreffende Kriterium eine eigene Meldung ausgibt, unabhängig davon, ob ein vorheriges Muster schon gepasst hat. Diese Technik ersetzt effektiv mehrere aufeinanderfolgende if-Blöcke, die alle denselben Wert prüfen, durch eine einzige, klar strukturierte case-Anweisung.
#!/usr/bin/env bash
set -euo pipefail
readonly FILE="report_2026_final.csv"
case "$FILE" in
*.csv)
echo "Format: CSV"
;;&
*_final.*)
echo "Marked as final version"
;;&
report_*)
echo "Matches report naming convention"
;;
esac
# All three matching branches run, because ;;& keeps testing further patterns
6. case für Argument-Parsing: extglob-Muster in der Praxis
Sobald shopt -s extglob aktiv ist, funktionieren dieselben erweiterten Musteroperatoren wie beim Datei-Globbing auch innerhalb von case-Zweigen. Der Operator @(...) lässt sich nutzen, um mehrere gleichwertige Kommandozeilenoptionen in einem einzigen Muster zusammenzufassen, während !(...) gezielt alles außer einem bestimmten Muster erfasst, etwa um alle Argumente außer bekannten Flags als ungültig zurückzuweisen.
In einer klassischen Argument-Parsing-Schleife mit while und shift übernimmt case typischerweise die Rolle, jedes einzelne Kommandozeilenargument gegen die Liste erlaubter Optionen zu prüfen. Die Kombination aus case und erweiterten Mustern macht solche Parser deutlich kompakter als eine entsprechende if/elif-Kette mit vielen einzelnen String-Vergleichen und regulären Ausdrücken.
#!/usr/bin/env bash
set -euo pipefail
shopt -s extglob
while (( $# > 0 )); do
case "$1" in
--verbose|-v)
readonly VERBOSE=1
shift
;;
--output=*|-o=*)
readonly OUTPUT="${1#*=}"
shift
;;
!(-*))
readonly TARGET="$1"
shift
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
7. Case-insensitive Matching mit shopt -s nocasematch
Standardmäßig unterscheidet case streng zwischen Groß- und Kleinschreibung, sodass ein Muster yes die Eingabe Yes nicht erfasst. Mit shopt -s nocasematch ändert sich dieses Verhalten global für alle nachfolgenden case-Statements und auch für [[ ]]-Musterabgleiche, sodass Groß- und Kleinschreibung bei allen Mustern ignoriert wird, bis die Option wieder mit shopt -u nocasematch deaktiviert wird.
Weil nocasematch global für die ganze Shell-Sitzung oder das ganze Skript gilt, empfiehlt es sich, die Option unmittelbar vor dem betroffenen case-Block zu aktivieren und direkt danach wieder zu deaktivieren, statt sie am Skriptanfang pauschal zu setzen. Andernfalls verhalten sich auch spätere, eigentlich case-sensitive gemeinte Vergleiche im Skript unerwartet großzügig, was zu schwer auffindbaren Logikfehlern führen kann.
#!/usr/bin/env bash
set -euo pipefail
readonly ANSWER="Yes"
shopt -s nocasematch
case "$ANSWER" in
yes)
echo "Confirmed (case-insensitive match)"
;;
no)
echo "Declined"
;;
esac
shopt -u nocasematch
8. Quoting-Fallstricke: wann ein Muster literal und wann als Glob gilt
Ein häufig übersehener Punkt ist, dass ungequotete Zeichen in einem case-Muster immer als Glob-Metazeichen interpretiert werden, während gequotete Teile literal behandelt werden. Ein Muster wie *.txt matcht jede Endung, aber "*.txt" in Anführungszeichen sucht nach der buchstäblichen Zeichenkette *.txt, was fast nie das gewünschte Verhalten ist. Wer versehentlich Sonderzeichen wie *, ? oder [ literal matchen will, muss diese gezielt escapen, etwa mit \*, statt das ganze Muster zu quoten.
Ein zweiter Fallstrick betrifft Variablen innerhalb eines Musters: Der Wert einer expandierten Variable wird, anders als man erwarten könnte, weiterhin als Glob-Muster interpretiert, sofern er ungequotet im Pattern steht, sodass ein Variablenwert mit einem enthaltenen * ungewollt zum Wildcard wird. Um eine Variable garantiert literal zu vergleichen, empfiehlt sich entweder eine explizite Quotierung des Variablenwerts oder ein vorheriger Test mit [[ "$a" == "$b" ]], wo beide Seiten als reine Zeichenketten behandelt werden können.
9. case als Alternative zu langen if/elif-Ketten
Eine lange if/elif-Kette mit zehn oder mehr Zweigen wird schnell unübersichtlich, besonders wenn jeder Zweig denselben Ausdruck wiederholt prüft, etwa if [[ $x == a ]]; elif [[ $x == b ]]; elif [[ $x == c ]]. Ein äquivalentes case-Statement liest sich deutlich klarer, weil der geprüfte Ausdruck nur einmal in der Kopfzeile steht und jeder Zweig ausschließlich das jeweilige Muster zeigt, ohne wiederholten Vergleichsoperator und wiederholte Variable.
Für sehr viele mögliche Werte, etwa mehr als zwanzig unterschiedliche Kommandos in einem Dispatcher-Skript, lohnt sich zusätzlich ein Blick auf assoziative Arrays als Dispatch-Tabelle, die Funktionsnamen direkt Schlüsseln zuordnen und in konstanter Zeit nachschlagen, während sowohl case als auch if/elif die Muster im schlechtesten Fall der Reihe nach durchgehen, bis eines passt.
| Mechanismus | Pattern-Typ | Performance bei vielen Zweigen | Typischer Einsatz |
|---|---|---|---|
case |
Glob-Muster, ein Ausdruck einmal ausgewertet | Linear, aber ein Ausdruck | Kommando-Dispatch, Argument-Parsing |
if/elif-Kette |
Beliebige Testausdrücke | Linear, jeder Zweig eigener Test | Wenige, komplexe Bedingungen |
| Assoziatives Array als Dispatch | Exakter Schlüssel, kein Pattern | Konstante Zeit | Viele feste Kommandos, Funktionsnamen |
select |
Nummerierte Menüoptionen | Linear, interaktiv | Interaktive Shell-Menüs |
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte, die in der Produktion zuverlässig laufen?
Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.
Code-Review
ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.
Refactoring
Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.
CI-Integration
ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.
10. Zusammenfassung
case-Pattern-Matching in Bash: Das Wichtigste auf einen Blick
Mehrere Pattern
Das Pipe-Zeichen | kombiniert mehrere gleichwertige Muster in einem Zweig, spezifische Muster gehören vor allgemeine Wildcards.
Fallthrough
;& führt bedingungslos den nächsten Zweig aus, ;;& prüft die folgenden Muster regulär weiter und führt nur echte Treffer aus.
Erweiterte Muster
Mit aktivem extglob funktionieren @(...), !(...) und weitere Operatoren auch als case-Pattern, ideal für Argument-Parsing.
Wann case statt if/elif
Ab etwa vier bis fünf Zweigen mit demselben geprüften Ausdruck ist case lesbarer und effizienter als eine if/elif-Kette.