Bedingungen sind das Rückgrat jedes Shell-Skripts: Sie entscheiden, welcher Codepfad genommen wird. Bash denkt dabei anders als die meisten Programmiersprachen — nicht der Wahrheitswert eines Ausdrucks zählt, sondern der Exit-Code eines Befehls. Wer das einmal verinnerlicht hat, versteht auch, warum es test, [ ] und [[ ]] parallel gibt und welcher Operator wann das richtige Werkzeug ist.
if, elif, else, fi
Die Grundsyntax einer Bash-Bedingung sieht so aus:
if befehl; then
echo "Befehl war erfolgreich"
elif anderer_befehl; then
echo "Zweiter Zweig"
else
echo "Keiner der Zweige hat gegriffen"
fiDas Strichpunkt vor then ist nötig, weil if und then sonst auf der gleichen Zeile als zwei separate Statements gelesen würden. Alternativ kann then auf einer eigenen Zeile stehen, dann entfällt der Strichpunkt.
Entscheidend ist: Hinter if steht kein boolescher Ausdruck, sondern ein Befehl. Bash führt diesen Befehl aus und prüft seinen Exit-Code:
- Exit-Code
0bedeutet “wahr” (Erfolg). - Jeder andere Exit-Code (typisch
1, aber auch2,127usw.) bedeutet “falsch”.
Das ist die umgekehrte Konvention zu C, Python oder JavaScript — dort gilt 0 als falsch. In der Shell ist 0 der Erfolgswert, weil ein Programm nur eine einzige Erfolgsart kennt, aber viele Fehlerarten haben kann.
if grep -q "ERROR" log.txt; then
echo "Fehler gefunden"
else
echo "Alles sauber"
fiAlles saubergrep -q gibt nichts auf stdout aus, sondern signalisiert nur über den Exit-Code, ob das Muster gefunden wurde. Genau so sind Shell-Bedingungen gedacht: Der Befehl ist die Wahrheit.
test, [ ] und [[ ]]
Wenn man etwas vergleichen will, was nicht direkt aus einem Befehl kommt — etwa zwei Strings oder eine Zahl —, braucht man ein Test-Konstrukt. Davon gibt es drei Varianten, die sich in Verbreitung und Mächtigkeit unterscheiden.
| Konstrukt | Typ | POSIX | Besonderheiten |
|---|---|---|---|
test AUSDRUCK | externes/eingebautes Kommando | ja | Klassische, aber heute selten genutzte Schreibweise |
[ AUSDRUCK ] | Alias für test | ja | Identisch zu test; das ] ist ein Argument, kein Syntax-Token |
[[ AUSDRUCK ]] | Bash-Builtin | nein | Erweiterte Features, sicherer und moderner |
Alle drei liefern einen Exit-Code, kein “Boolean”. [ -f datei ] ist also ein Befehl, der 0 zurückgibt, wenn die Datei existiert — und 1, wenn nicht.
test -f /etc/hosts
[ -f /etc/hosts ]
[[ -f /etc/hosts ]]Der wichtigste Unterschied: [[ ]] ist ein Sprachkonstrukt, kein Befehl. Dadurch macht Bash innerhalb von [[ ]] kein Word-Splitting und kein Glob-Expanding auf nicht-quotierte Variablen. Das macht den Code robuster — und gleichzeitig erlaubt [[ ]] Features, die mit [ ] gar nicht gehen: Glob-Match mit ==, Regex mit =~, sowie && und || direkt im Ausdruck.
In neuen Bash-Skripten gilt: [[ ]] ist die Standardwahl. [ ] braucht man, wenn das Skript POSIX-portabel sein soll (z. B. unter dash als /bin/sh).
String-Vergleiche
Strings vergleicht man mit =, !=, <, > sowie den unären Tests -z (zero, leer) und -n (non-zero, nicht leer). Innerhalb von [[ ]] ist zusätzlich == erlaubt — und übernimmt die spannende Doppelrolle als Glob-Match.
| Operator | Bedeutung | Verfügbar in |
|---|---|---|
= | gleich | [ ], [[ ]] (POSIX) |
== | gleich (Bash) bzw. Glob-Match | nur [[ ]] |
!= | ungleich bzw. negatives Glob-Match | [ ], [[ ]] |
< / > | lexikografisch kleiner/größer | [[ ]] (in [ ] muss man \< schreiben) |
-z STRING | String ist leer | [ ], [[ ]] |
-n STRING | String ist nicht leer | [ ], [[ ]] |
=~ REGEX | Regex-Match | nur [[ ]] |
name="Anna"
if [[ "$name" = "Anna" ]]; then
echo "Hallo, Anna"
fi
if [[ -z "$leer" ]]; then
echo "Variable ist leer oder ungesetzt"
fiDer Block zeigt zwei Standard-Tests: Gleichheit per = und Leertest per -z. Beachte die doppelten Anführungszeichen — sie sind in [[ ]] nicht zwingend, schaden aber nie und sind in [ ] Pflicht.
datei="bericht.pdf"
if [[ "$datei" == *.pdf ]]; then
echo "Es ist ein PDF"
fiIm rechten Operanden eines == darf in [[ ]] ein Glob-Pattern stehen. Wichtig: Das Muster steht ohne Quotes, sonst wird es als wörtlicher String interpretiert. "$datei" == "*.pdf" wäre exakt der String “*.pdf” — hier nicht gewollt.
eingabe="Version 12.4"
if [[ "$eingabe" =~ ^Version\ ([0-9]+)\.([0-9]+)$ ]]; then
echo "Major: ${BASH_REMATCH[1]}, Minor: ${BASH_REMATCH[2]}"
fi=~ matcht den linken Operanden gegen einen erweiterten regulären Ausdruck. Capture-Gruppen landen im Array BASH_REMATCH: Index 0 ist der Gesamttreffer, 1, 2, … sind die Gruppen.
Numerische Vergleiche
Für Zahlen gelten eigene Operatoren, weil < und > in den Test-Konstrukten lexikografisch arbeiten und damit z. B. "10" < "9" als wahr werten würden.
| Operator | Bedeutung |
|---|---|
-eq | gleich |
-ne | ungleich |
-lt | kleiner |
-le | kleiner oder gleich |
-gt | größer |
-ge | größer oder gleich |
anzahl=12
if [[ "$anzahl" -gt 10 ]]; then
echo "Mehr als zehn"
fiMehr als zehnAlternativ gibt es das arithmetische Konstrukt (( )), das mit normalen mathematischen Operatoren arbeitet — <, >, ==, !=. Innerhalb von (( )) braucht es keine $ vor Variablennamen:
if (( anzahl > 10 )); then
echo "Mehr als zehn"
fi(( )) ist die natürlichere Schreibweise für reine Zahl-Logik — Details und Rechen-Tricks stehen im Artikel zur Arithmetik in Bash.
Datei-Tests
Datei-Tests prüfen Existenz, Typ und Eigenschaften von Dateien und Verzeichnissen. Sie funktionieren in [ ] und [[ ]] gleich.
| Operator | Wahr, wenn … |
|---|---|
-e PFAD | Datei oder Verzeichnis existiert |
-f PFAD | reguläre Datei (kein Verzeichnis, kein Symlink-Ziel-Sonderfall) |
-d PFAD | Verzeichnis |
-L PFAD | symbolischer Link (auch wenn das Ziel fehlt) |
-r PFAD | Datei ist lesbar für den aktuellen User |
-w PFAD | Datei ist schreibbar |
-x PFAD | Datei ist ausführbar (Verzeichnis: betretbar) |
-s PFAD | Datei existiert und ist nicht leer |
-O PFAD | aktueller User ist Eigentümer |
-G PFAD | Datei gehört einer Gruppe des Users |
A -nt B | Datei A ist neuer als B (newer than) |
A -ot B | Datei A ist älter als B (older than) |
pfad="/etc/hosts"
if [[ -e "$pfad" ]]; then
echo "$pfad existiert"
fi
if [[ -f "$pfad" && -r "$pfad" ]]; then
echo "$pfad ist eine lesbare reguläre Datei"
fiDer erste Test fragt nur nach Existenz — egal ob Datei, Verzeichnis oder Symlink. Der zweite ist strenger: er verlangt eine reguläre Datei und Leserecht. Beachte das && direkt im [[ ]] — das ist nur dort erlaubt.
if [[ -L "$pfad" ]]; then
echo "$pfad ist ein Symlink"
elif [[ -d "$pfad" ]]; then
echo "$pfad ist ein Verzeichnis"
elif [[ -f "$pfad" ]]; then
echo "$pfad ist eine reguläre Datei"
fi-L testet auf den Symlink selbst, ohne ihm zu folgen. -f und -d folgen dagegen dem Link — ein Symlink auf ein Verzeichnis ist mit -d wahr. Wenn man explizit den Link-Typ braucht, muss -L zuerst geprüft werden.
Bedingungen kombinieren
Innerhalb von [[ ]] kombiniert man Teilbedingungen mit den Bash-Operatoren && (UND) und || (ODER). Beide arbeiten kurzschließend: bei A && B wird B nur dann ausgewertet, wenn A wahr ist; bei A || B nur, wenn A falsch ist.
if [[ -f "$pfad" && -s "$pfad" ]]; then
echo "Datei existiert und ist nicht leer"
fi
if [[ "$mode" = "dev" || "$mode" = "test" ]]; then
echo "Nicht-produktive Umgebung"
fiAußerhalb von Tests sind && und || Befehlsverkettungen: cmd1 && cmd2 führt cmd2 nur aus, wenn cmd1 mit 0 endet; cmd1 || cmd2 umgekehrt. Das ist das Bash-Idiom schlechthin.
mkdir -p build && cp -r src/* build/
command -v jq >/dev/null || { echo "jq fehlt"; exit 1; }Die erste Zeile baut das Verzeichnis und kopiert nur, wenn mkdir erfolgreich war. Die zweite prüft, ob jq installiert ist — falls nicht, wird in einer Gruppe { ...; } eine Fehlermeldung gedruckt und das Skript beendet.
Befehl direkt als Bedingung
Eines der wichtigsten Bash-Idiome ist, den Befehl selbst in das if zu schreiben, statt seine Ausgabe in einen Test zu stecken.
if [ "$(grep ERROR log.txt)" != "" ]; then
echo "Fehler gefunden"
fiif grep -q ERROR log.txt; then
echo "Fehler gefunden"
fiDie zweite Variante ist kürzer, schneller (kein Subshell-Spawn, kein String-Vergleich) und drückt klarer die Absicht aus: “wenn grep fündig wird”. -q (quiet) unterdrückt zusätzlich die Ausgabe.
if command -v docker >/dev/null; then
echo "Docker ist installiert"
fi
if ping -c 1 -W 1 example.com >/dev/null 2>&1; then
echo "Netzwerk erreichbar"
ficommand -v X ist die portable Art zu prüfen, ob ein Programm im PATH liegt — sauberer als which. Beim ping nutzen wir >/dev/null 2>&1, um sowohl stdout als auch stderr zu verwerfen, weil uns nur der Exit-Code interessiert.
Praxis-Patterns
Eine Sammlung kleiner Bausteine, die in fast jedem Skript auftauchen — jeweils mit kurzer Erläuterung.
config="/etc/myapp/config.yaml"
if [[ ! -f "$config" ]]; then
echo "Config fehlt: $config" >&2
exit 1
fiKlassischer Vorlauf eines Skripts: Falls die Konfigurationsdatei fehlt, wird auf stderr (>&2) eine Meldung ausgegeben und mit Exit-Code 1 abgebrochen. Das ! negiert den Test.
if [[ -z "${API_TOKEN:-}" ]]; then
echo "API_TOKEN ist nicht gesetzt" >&2
exit 1
fi${API_TOKEN:-} liefert den Inhalt von API_TOKEN oder einen leeren String, wenn die Variable ungesetzt ist. Damit funktioniert der -z-Test auch unter set -u (nounset), das sonst beim Zugriff auf ungesetzte Variablen aussteigt.
for eintrag in /var/log/*; do
if [[ -d "$eintrag" ]]; then
echo "DIR : $eintrag"
elif [[ -f "$eintrag" ]]; then
echo "FILE: $eintrag"
fi
doneDie Schleife geht alle Einträge in /var/log durch und sortiert sie nach Typ. -d und -f schließen sich wechselseitig aus, was die Verzweigung sauber lesbar macht.
port="${PORT:-8080}"
echo "Starte auf Port $port"${PORT:-8080} ist kein Test, sondern eine Parameter-Expansion, ersetzt aber sehr oft ein eigenes if. Mehr zu diesen Mustern steht im Artikel zur String-Manipulation.
if (( score >= 0 && score <= 100 )); then
echo "Score im erlaubten Bereich"
else
echo "Score außerhalb 0-100" >&2
fiIm arithmetischen Kontext lassen sich Bereichschecks natürlich schreiben. Wer das in [[ ]] macht, müsste [[ "$score" -ge 0 && "$score" -le 100 ]] schreiben — funktional gleich, aber sperriger.
Stolperfallen
Variablen ohne Quotes in eckigen Klammern
In [ ] werden Variablen wie überall in Bash word-gesplittet. Wenn x leer ist, wird aus [ $x = foo ] nach Expansion [ = foo ] — ein Syntaxfehler, weil dem = der linke Operand fehlt. Korrekt ist [ "$x" = foo ]. In [[ ]] passiert dieses Splitting nicht: [[ $x = foo ]] funktioniert auch bei leerem x. Trotzdem ist es saubere Praxis, Variablen immer zu quoten — das macht den Code portierbar und schützt vor Verwechslungen mit anderen Konstrukten.
= vs. == in [ ] ist POSIX-Konflikt
POSIX kennt nur = für String-Gleichheit; == ist eine Bash-Erweiterung. In [ ] funktioniert == zwar in Bash, aber nicht in dash oder sh. Wer ein Skript portabel halten will (Shebang #!/bin/sh), muss = verwenden. In [[ ]] darf man beides; üblich ist ==.
-a und -o in [ ] sind deprecated
Die kombinierten Tests [ A -a B ] (UND) und [ A -o B ] (ODER) gelten als veraltet und können bei mehrdeutigen Operanden zu falschen Ergebnissen führen. Empfohlen ist stattdessen [ A ] && [ B ] bzw. [ A ] || [ B ] — oder gleich [[ A && B ]] mit dem Bash-Konstrukt. Aktuelle POSIX-Versionen markieren -a/-o als “obsolescent”.
Leeren String testen: -z statt = ''
[ "$x" = "" ] und [ -z "$x" ] machen das Gleiche, aber -z ist die idiomatische Form: ein Wort statt drei, klarer Intent, funktioniert auch ohne korrekt gequotete Vergleichsseite. Analog -n "$x" für “nicht leer”. Wer mit ${VAR:-} arbeitet, schützt sich gleichzeitig gegen unset-Variablen unter set -u.
Exit-Code 1 ist 'falsch' — auch für Datei-Tests
[[ -e /nicht/da ]] liefert Exit-Code 1, weil die Datei nicht existiert — das ist die Konvention für “Test war negativ”. Verwirrend wird es, wenn man den Exit-Code per echo $? direkt anschaut: 1 sieht aus wie ein Fehler, ist hier aber das normale “false”-Signal. Echte Test-Fehler (z. B. Syntaxfehler im Ausdruck) liefern 2. Kurz: 0 wahr, 1 falsch, 2 Fehler.
Glob-Pattern in == nicht quoten
Innerhalb von [[ ]] ist [[ "$datei" == *.txt ]] ein Glob-Match. Sobald man das Muster quotet — [[ "$datei" == "*.txt" ]] —, wird daraus ein wörtlicher Stringvergleich gegen die Zeichenfolge “*.txt”, der nur bei exakt diesem Dateinamen wahr ist. Diese Falle ist der klassische “warum matcht mein Pattern nicht”-Bug. Linke Seite quoten, rechte Seite (das Pattern) nicht — das ist die Regel.
if liest den Befehl, nicht die Ausgabe
Anfänger schreiben oft if [ $(cmd) ]; then ... und meinen damit “wenn der Befehl erfolgreich war”. Tatsächlich wird hier die Ausgabe des Befehls als Test-Argument benutzt — if selbst hätte längst den Exit-Code von [ ] gelesen, nicht von cmd. Korrekt ist if cmd; then .... Dieser Unterschied ist die häufigste Quelle für scheinbar unerklärliche Logik-Fehler in Skripten.
Weiterführende Ressourcen
Externe Quellen
- Bash-Manpage: CONDITIONAL EXPRESSIONS — Vollständige Liste aller Test-Operatoren
- GNU Bash-Handbuch: Conditional Constructs — Offizielle Dokumentation zu
if,[[ ]],caseu. a. - POSIX: test utility — Was
[ ]portabel können muss - Bash Hackers Wiki: Classic test command — Praktische Übersicht zu
[ ]und[[ ]] - ShellCheck — Statische Analyse, fängt fast alle hier beschriebenen Fallen automatisch
Verwandte Artikel
- Script-Grundlagen — Shebang,
set -euo pipefail, robuste Skripte - Variablen — Shell- und Umgebungsvariablen, Quoting, Expansion
- Schleifen —
for,while,untilund ihre Stolperfallen - case-Anweisung — Pattern-Matching als Alternative zu vielen
elif - Funktionen — Wiederverwendbare Logik mit Rückgabewerten und Argumenten