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:

Bash if-Grundsyntax
if befehl; then
    echo "Befehl war erfolgreich"
elif anderer_befehl; then
    echo "Zweiter Zweig"
else
    echo "Keiner der Zweige hat gegriffen"
fi

Das 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 0 bedeutet “wahr” (Erfolg).
  • Jeder andere Exit-Code (typisch 1, aber auch 2, 127 usw.) 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.

Bash Exit-Code als Bedingung
if grep -q "ERROR" log.txt; then
    echo "Fehler gefunden"
else
    echo "Alles sauber"
fi
Output
Alles sauber

grep -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.

KonstruktTypPOSIXBesonderheiten
test AUSDRUCKexternes/eingebautes KommandojaKlassische, aber heute selten genutzte Schreibweise
[ AUSDRUCK ]Alias für testjaIdentisch zu test; das ] ist ein Argument, kein Syntax-Token
[[ AUSDRUCK ]]Bash-BuiltinneinErweiterte 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.

Bash Drei Varianten desselben Tests
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.

OperatorBedeutungVerfügbar in
=gleich[ ], [[ ]] (POSIX)
==gleich (Bash) bzw. Glob-Matchnur [[ ]]
!=ungleich bzw. negatives Glob-Match[ ], [[ ]]
< / >lexikografisch kleiner/größer[[ ]] (in [ ] muss man \< schreiben)
-z STRINGString ist leer[ ], [[ ]]
-n STRINGString ist nicht leer[ ], [[ ]]
=~ REGEXRegex-Matchnur [[ ]]
Bash Einfache String-Vergleiche
name="Anna"
if [[ "$name" = "Anna" ]]; then
    echo "Hallo, Anna"
fi

if [[ -z "$leer" ]]; then
    echo "Variable ist leer oder ungesetzt"
fi

Der 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.

Bash Glob-Match mit ==
datei="bericht.pdf"
if [[ "$datei" == &#42;.pdf ]]; then
    echo "Es ist ein PDF"
fi

Im rechten Operanden eines == darf in [[ ]] ein Glob-Pattern stehen. Wichtig: Das Muster steht ohne Quotes, sonst wird es als wörtlicher String interpretiert. "$datei" == "&#42;.pdf" wäre exakt der String “*.pdf” — hier nicht gewollt.

Bash Regex-Match mit =~
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.

OperatorBedeutung
-eqgleich
-neungleich
-ltkleiner
-lekleiner oder gleich
-gtgrößer
-gegrößer oder gleich
Bash Numerischer Vergleich mit [[ ]]
anzahl=12
if [[ "$anzahl" -gt 10 ]]; then
    echo "Mehr als zehn"
fi
Output
Mehr als zehn

Alternativ gibt es das arithmetische Konstrukt (( )), das mit normalen mathematischen Operatoren arbeitet — <, >, ==, !=. Innerhalb von (( )) braucht es keine $ vor Variablennamen:

Bash Numerischer Vergleich mit (( ))
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.

OperatorWahr, wenn …
-e PFADDatei oder Verzeichnis existiert
-f PFADreguläre Datei (kein Verzeichnis, kein Symlink-Ziel-Sonderfall)
-d PFADVerzeichnis
-L PFADsymbolischer Link (auch wenn das Ziel fehlt)
-r PFADDatei ist lesbar für den aktuellen User
-w PFADDatei ist schreibbar
-x PFADDatei ist ausführbar (Verzeichnis: betretbar)
-s PFADDatei existiert und ist nicht leer
-O PFADaktueller User ist Eigentümer
-G PFADDatei gehört einer Gruppe des Users
A -nt BDatei A ist neuer als B (newer than)
A -ot BDatei A ist älter als B (older than)
Bash Datei-Existenz und -Typ prüfen
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"
fi

Der 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.

Bash Symlink von Ziel unterscheiden
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.

Bash UND/ODER innerhalb von [[ ]]
if [[ -f "$pfad" && -s "$pfad" ]]; then
    echo "Datei existiert und ist nicht leer"
fi

if [[ "$mode" = "dev" || "$mode" = "test" ]]; then
    echo "Nicht-produktive Umgebung"
fi

Auß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.

Bash Klassische Idiome ohne if
mkdir -p build && cp -r src/* build/
command -v jq &#62;/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.

Bash Falsch: unnötige Subshell
if [ "$(grep ERROR log.txt)" != "" ]; then
    echo "Fehler gefunden"
fi
Bash Idiomatisch: Exit-Code direkt
if grep -q ERROR log.txt; then
    echo "Fehler gefunden"
fi

Die 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.

Bash Weitere typische Patterns
if command -v docker &#62;/dev/null; then
    echo "Docker ist installiert"
fi

if ping -c 1 -W 1 example.com &#62;/dev/null 2&#62;&#38;1; then
    echo "Netzwerk erreichbar"
fi

command -v X ist die portable Art zu prüfen, ob ein Programm im PATH liegt — sauberer als which. Beim ping nutzen wir &#62;/dev/null 2&#62;&#38;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.

Bash Datei-Existenz und Schreibrecht prüfen
config="/etc/myapp/config.yaml"

if [[ ! -f "$config" ]]; then
    echo "Config fehlt: $config" &#62;&#38;2
    exit 1
fi

Klassischer Vorlauf eines Skripts: Falls die Konfigurationsdatei fehlt, wird auf stderr (&#62;&#38;2) eine Meldung ausgegeben und mit Exit-Code 1 abgebrochen. Das ! negiert den Test.

Bash Pflicht-Variable prüfen
if [[ -z "${API_TOKEN:-}" ]]; then
    echo "API_TOKEN ist nicht gesetzt" &#62;&#38;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.

Bash Verzeichnis vs. Datei unterscheiden
for eintrag in /var/log/&#42;; do
    if [[ -d "$eintrag" ]]; then
        echo "DIR : $eintrag"
    elif [[ -f "$eintrag" ]]; then
        echo "FILE: $eintrag"
    fi
done

Die 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.

Bash Default-Wert fallback
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.

Bash Numerischer Bereichscheck
if (( score &#62;= 0 && score &#60;= 100 )); then
    echo "Score im erlaubten Bereich"
else
    echo "Score außerhalb 0-100" &#62;&#38;2
fi

Im 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" == &#42;.txt ]] ein Glob-Match. Sobald man das Muster quotet — [[ "$datei" == "&#42;.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

  • Script-Grundlagen — Shebang, set -euo pipefail, robuste Skripte
  • Variablen — Shell- und Umgebungsvariablen, Quoting, Expansion
  • Schleifenfor, while, until und ihre Stolperfallen
  • case-Anweisung — Pattern-Matching als Alternative zu vielen elif
  • Funktionen — Wiederverwendbare Logik mit Rückgabewerten und Argumenten
/ Weiter

Zurück zu Shell-Scripting

Zur Übersicht