Bash-Bedingungsvergleiche: Wann man test, [ und [[ verwendet

Vergleiche test, einfache Klammern und doppelte Klammern, damit deine Bash-Bedingungen portabel, sicher und lesbar bleiben.

Bash-Bedingungsvergleiche: Wann man test, [ und [[ verwendet

Wenn sich eine Bash-Bedingung seltsam verhält, liegt das Problem oft an der gewählten Konstruktion. test, [ ] und [[ ]] sehen ähnlich aus, aber sie behandeln Anführungszeichen, Muster, reguläre Ausdrücke und Portabilität unterschiedlich.

Dieser Leitfaden vergleicht die drei Formen, damit du Bedingungen schreiben kannst, die sicher, lesbar und für die Shell geeignet sind, unter der dein Skript tatsächlich läuft.

Der test-Befehl: Die Grundlage

Der test-Befehl ist eine der ältesten und grundlegendsten Methoden, um Bedingungen in Shell-Skripten auszuwerten. Er ist ein eingebauter Befehl in den meisten modernen Shells und Teil des POSIX-Standards, was ihn sehr portabel macht. test wertet einen Ausdruck aus und gibt einen Exit-Status von 0 (wahr) oder 1 (falsch) zurück.

Grundlegende Verwendung

Der test-Befehl nimmt ein oder mehrere Argumente entgegen, die den auszuwertenden Ausdruck bilden. Er prüft Dateiattribute, Zeichenkettenvergleiche und Integer-Vergleiche.

# Prüfen, ob eine Datei existiert
if test -f "myfile.txt"; then
    echo "myfile.txt existiert und ist eine reguläre Datei."
fi

# Prüfen, ob zwei Zeichenketten gleich sind
NAME="Alice"
if test "$NAME" = "Alice"; then
    echo "Name ist Alice."
fi

# Prüfen, ob eine Zahl größer als eine andere ist
COUNT=10
if test "$COUNT" -gt 5; then
    echo "Anzahl ist größer als 5."
fi

Häufige test-Operatoren

  • Datei-Operatoren: -f (reguläre Datei), -d (Verzeichnis), -e (existiert), -s (nicht leer), -r (lesbar), -w (schreibbar), -x (ausführbar).
  • Zeichenketten-Operatoren: = (gleich), != (ungleich), -z (Zeichenkette ist leer), -n (Zeichenkette ist nicht leer).
  • Integer-Operatoren: -eq (gleich), -ne (ungleich), -gt (größer als), -ge (größer oder gleich), -lt (kleiner als), -le (kleiner oder gleich).

Tipp: Setze Variablen, die mit test verwendet werden, immer in Anführungszeichen (z.B. "$NAME"), um Probleme mit Wortteilung und Pfadnamenerweiterung zu vermeiden, falls der Wert der Variablen Leerzeichen oder Glob-Zeichen enthält.

Einfache Klammern [ ]: Die test-Form

Die Konstruktion mit einfachen Klammern [ ] ist eine alternative Syntax für den test-Befehl. In vielen Shells ist [ ein eingebauter Befehl der Shell, und Systeme bieten oft auch ein externes /usr/bin/[. Der Hauptunterschied besteht darin, dass [ ein schließendes ] als letztes Argument erfordert. Wie test ist es POSIX-konform.

Syntax und Semantik

# Äquivalent zu test -f "myfile.txt"
if [ -f "myfile.txt" ]; then
    echo "myfile.txt existiert und ist eine reguläre Datei mit [ ]."
fi

# Äquivalent zu test "$NAME" = "Alice"
NAME="Bob"
if [ "$NAME" != "Alice" ]; then
    echo "Name ist nicht Alice."
fi

Beachte das obligatorische Leerzeichen nach [ und vor ]. Diese werden als separate Argumente für den [-Befehl behandelt.

Variablen in Anführungszeichen setzen: Ein kritisches Detail

Da [ ] im Grunde der test-Befehl ist, erbt es die gleichen Verhaltensweisen bezüglich Wortteilung und Pfadnamenerweiterung. Das bedeutet, dass nicht in Anführungszeichen gesetzte Variablen zu unerwartetem Verhalten oder Sicherheitslücken führen können.

Betrachte dieses Beispiel:

#!/bin/bash

INPUT="Datei mit Leerzeichen.txt"

# GEFÄHRLICH: Nicht in Anführungszeichen gesetzte Variable verursacht Probleme, wenn INPUT Leerzeichen enthält
# Die Shell führt eine Wortteilung durch und behandelt "Datei" und "mit Leerzeichen.txt" als separate Argumente,
# was zu einem Syntaxfehler oder einer falschen Auswertung führt.
# if [ -f $INPUT ]; then echo "Gefunden"; else echo "Nicht gefunden"; fi 

# RICHTIG: Setze die Variable in Anführungszeichen, um sie als ein einzelnes Argument zu behandeln
if [ -f "$INPUT" ]; then
    echo "'Datei mit Leerzeichen.txt' existiert."
else
    echo "'Datei mit Leerzeichen.txt' existiert nicht oder ist keine reguläre Datei."
fi

Ohne Anführungszeichen würde $INPUT zu Datei mit Leerzeichen.txt expandieren, und [ -f Datei mit Leerzeichen.txt ] würde vom [-Befehl als Syntaxfehler interpretiert, da -f nur einen Operanden erwartet. Anführungszeichen stellen sicher, dass $INPUT als ein einzelnes Argument, "Datei mit Leerzeichen.txt", übergeben wird.

Gefahren der Wortteilung und Pfadnamenerweiterung

Sowohl test als auch [ unterliegen den Standardverhaltensweisen der Shell bezüglich Wortteilung und Pfadnamenerweiterung (Globbing). Wenn eine Variable Leerzeichen oder Glob-Zeichen (*, ?, [ ]) enthält und nicht in Anführungszeichen gesetzt ist, wird die Shell sie expandieren, bevor test oder [ die Argumente sehen. Dies kann zu Syntaxfehlern oder falschen Vergleichen führen, wenn Glob-Zeichen mit vorhandenen Dateien übereinstimmen.

Doppelte Klammern [[ ]]: Das moderne Bash-Schlüsselwort

Die Konstruktion mit doppelten Klammern [[ ]] ist ein Bash-Schlüsselwort (auch von Ksh und Zsh unterstützt), kein externer Befehl oder Alias. Diese Unterscheidung ist entscheidend, da sie [[ ]] erlaubt, sich anders zu verhalten und erweiterte Funktionalität sowie verbesserte Sicherheit im Vergleich zu test oder [ ] zu bieten.

Erweiterte Funktionalität

[[ ]] führt mehrere leistungsstarke Funktionen ein, die mit test oder [ nicht verfügbar sind:

  1. Keine Wortteilung oder Pfadnamenerweiterung: Variablen innerhalb von [[ ]] müssen im Allgemeinen nicht in Anführungszeichen gesetzt werden (obwohl es oft gute Praxis ist, dies aus Gründen der Klarheit zu tun). Die Shell behandelt den Inhalt von [[ ]] als eine einzelne Einheit, wodurch Wortteilung und Pfadnamenerweiterung verhindert werden. Dies reduziert häufige Skriptfehler und Sicherheitsrisiken erheblich.

    # Keine Notwendigkeit, Variablen in Anführungszeichen zu setzen (obwohl es immer noch sicher ist)
    INPUT="Datei mit Leerzeichen.txt"
    if [[ -f $INPUT ]]; then # $INPUT wird hier als einzelne Zeichenkette behandelt
        echo "'$INPUT' existiert."
    fi
    
  2. Globbing für Zeichenkettenvergleich: Die Operatoren == und != führen Mustervergleiche (Globbing) durch, wenn sie innerhalb von [[ ]] verwendet werden, anstelle eines strengen Zeichenkettenvergleichs. Das bedeutet, du kannst *, ? und [] als Platzhalter verwenden.

    DATEINAME="mein_dokument.txt"
    if [[ "$DATEINAME" == *".txt" ]]; then # Prüft, ob DATEINAME mit .txt endet
        echo "Es ist eine Textdatei!"
    fi
    
    # Hinweis: Für einen strengen Zeichenkettenvergleich ohne Globbing verwende `test` oder `[ ]` mit `=`
    # oder stelle sicher, dass auf der rechten Seite von `==` in [[ ]] keine Glob-Zeichen vorhanden sind
    # (oder setze die rechte Seite in Anführungszeichen, wenn sie wörtliche Glob-Zeichen enthält, die du wörtlich vergleichen möchtest).
    
  3. Reguläre Ausdrücke abgleichen: Der Operator =~ ermöglicht das Abgleichen von regulären Ausdrücken.

    IP_ADRESSE="192.168.1.100"
    if [[ "$IP_ADRESSE" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        echo "Gültiges IP-Format."
    fi

    # Wichtig: Das Regex-Muster auf der rechten Seite von =~ sollte im Allgemeinen NICHT in Anführungszeichen gesetzt werden,
    # wenn es Zeichen enthält, die sonst als Glob-Muster behandelt würden.
    # Wenn der Regex in einer Variable ist, sollte er ebenfalls nicht in Anführungszeichen gesetzt werden.
    # Beispiel für ein Muster: ^[A-Za-z]+$
    ```

4.  **Logische Operatoren `&&` und `||`**: `[[ ]]` unterstützt die intuitiveren C-artigen logischen Operatoren `&&` (UND) und `||` (ODER) zum Kombinieren mehrerer Bedingungen, zusammen mit `!` für die Negation. Diese Operatoren haben eine korrekte Kurzschlussauswertung und Priorität, im Gegensatz zu `test`'s `-a` und `-o`.

    ```bash
    ALTER=25
    if [[ "$NAME" == "Alice" && "$ALTER" -ge 18 ]]; then
        echo "Alice ist erwachsen."
    fi

    if [[ "$USER" == "root" || -w /etc/fstab ]]; then
        echo "Entweder root oder kann in /etc/fstab schreiben."
    fi
    ```

### Bash-spezifische Natur

Obwohl `[[ ]]` erhebliche Vorteile bietet, ist sein Hauptnachteil, dass es eine Bash/Ksh/Zsh-Erweiterung ist und nicht Teil des POSIX-Standards. Das bedeutet, dass Skripte, die auf `[[ ]]` angewiesen sind, möglicherweise nicht auf `sh`, `dash` oder ältere/minimale Unix-ähnliche Systeme portierbar sind.

## Seitenvergleich: `test` vs. `[` vs. `[[`

Hier ist eine Tabelle, die die wichtigsten Unterschiede zusammenfasst:

| Merkmal                      | `test`                           | `[ ]`                               | `[[ ]]`                                   |
| :--------------------------- | :------------------------------- | :---------------------------------- | :---------------------------------------- |
| **Typ**                      | Eingebauter Befehl (oder extern) | Eingebauter Befehl (Alias für `test`) | Shell-Schlüsselwort (Bash, Ksh, Zsh)      |
| **POSIX-konform**            | Ja                               | Ja                                  | Nein                                      |
| **Schließendes `]` erforderlich** | Nein                             | Ja (als letztes Argument)           | Ja (als Teil des Schlüsselworts)          |
| **Wortteilung**              | Ja, bei nicht in Anführungszeichen gesetzten Variablen | Ja, bei nicht in Anführungszeichen gesetzten Variablen | Nein, Variablen werden als einzelne Zeichenketten behandelt |
| **Pfadnamenerweiterung**     | Ja, bei nicht in Anführungszeichen gesetzten Variablen | Ja, bei nicht in Anführungszeichen gesetzten Variablen | Nein |
| **Globbing-Mustervergleich** | Nein für Zeichenkettenvergleich  | Nein für Zeichenkettenvergleich     | Ja mit nicht in Anführungszeichen gesetzter rechter Seite von `==` oder `!=` |
| **Reguläre Ausdrücke**       | Nein                             | Nein                                | Ja mit `=~` |
| **Logisches UND/ODER**       | `-a`, `-o` existieren, sind aber leicht falsch zu lesen | `-a`, `-o` existieren, sind aber leicht falsch zu lesen | `&&`, `||` mit normalem Kurzschlussverhalten |
| **Zusammengesetzte Befehle** | Erfordert separate `test`-Aufrufe | Erfordert separate `[`-Aufrufe      | Kann Ausdrücke direkt kombinieren (`&&`/`||`)|
| **Variablen in Anführungszeichen setzen** | **Zwingend erforderlich** für Sicherheit | **Zwingend erforderlich** für Sicherheit | Im Allgemeinen nicht erforderlich, aber gute Praxis |

## Wann man was verwendet

Die Wahl der richtigen Bedingungskonstruktion hängt in erster Linie von deinen Portabilitätsanforderungen und der Komplexität deiner Bedingungslogik ab.

### POSIX-Konformität vs. moderne Bash-Funktionen

-   **Verwende `test` oder `[ ]`, wenn...**
    -   **Portabilität von größter Bedeutung ist**: Wenn dein Skript auf jeder POSIX-konformen Shell (`sh`, `dash`, ältere Systeme usw.) laufen muss, sind `test` oder `[ ]` deine einzigen zuverlässigen Optionen.
    -   Deine Bedingungen einfach sind (Dateiprüfungen, grundlegende Zeichenketten-/Integer-Vergleiche).
    -   Du mit sorgfältigem Setzen von Anführungszeichen bei allen Variablen und der Verwendung von `&&`/`||` auf Shell-Ebene außerhalb der Klammern vertraut bist, wenn du zusammengesetzte Logik benötigst.

-   **Verwende `[[ ]]`, wenn...**
    -   **Du ausschließlich für Bash schreibst** (oder Ksh/Zsh) und keine POSIX-Portabilität benötigst.
    -   Du erweiterte Funktionen wie Globbing-Mustervergleich, Abgleich regulärer Ausdrücke oder C-artige `&&`/`||`-logische Operatoren benötigst.
    -   Du die verbesserten Sicherheitsfunktionen wünschst, die Wortteilung und Pfadnamenerweiterung verhindern, was zu robusterem und weniger fehleranfälligem Code führt.
    -   Deine Bedingungen komplexe Logik beinhalten, die mit `test -a`/`-o` umständlich wäre.

### Best Practices und Empfehlungen

1.  **Bevorzuge `[[ ]]` für Bash-Skripte**: Wenn dein Skript für Bash bestimmt ist, ist `[[ ]]` im Allgemeinen die bevorzugte Wahl aufgrund seiner erhöhten Sicherheit, erweiterten Funktionalität und intuitiveren Syntax für komplexe Bedingungen. Es reduziert drastisch häufige Skriptfehler im Zusammenhang mit Anführungszeichen und Sonderzeichen.

2.  **Setze in `test` und `[ ]` immer Anführungszeichen**: Wenn du `test` oder `[ ]` aus POSIX-Konformitätsgründen *verwenden musst*, mache es dir zur Gewohnheit, **deine Variablen immer in Anführungszeichen zu setzen**, um unerwartetes Verhalten durch Wortteilung und Pfadnamenerweiterung zu verhindern.

    ```bash
    # Gute Praxis für [ ] und test
    VAR="eine Zeichenkette mit Leerzeichen"
    if [ -n "$VAR" ]; then echo "Nicht leer"; fi
    ```

3.  **Achte auf Mustervergleiche**: In `test` und `[ ]` wird `=` für Zeichenkettenvergleiche verwendet. In `[[ ]]` können `=` und `==` beide Mustervergleiche durchführen, wenn die rechte Seite nicht in Anführungszeichen gesetzt ist. Setze die rechte Seite in Anführungszeichen, wenn du einen wörtlichen Zeichenkettenvergleich wünschst.

4.  **Reguläre Ausdrücke mit `=~`**: Wenn du `=~` in `[[ ]]` verwendest, sollte die rechte Seite typischerweise nicht in Anführungszeichen gesetzt werden, damit die Shell sie als reguläres Ausdrucksmuster interpretieren kann, nicht als wörtliche Zeichenkette zum Abgleichen.

    ```bash
    # Nicht in Anführungszeichen gesetztes Regex-Muster ist korrekt für =~ in [[ ]]
    if [[ "$LINE" =~ ^Error: ]]; then echo "Fehler gefunden"; fi
    ```

## Fazit

Verwende `[ ]` oder `test`, wenn dein Skript unter POSIX `sh` laufen muss. Verwende `[[ ]]`, wenn dein Shebang Bash ist und du eine sicherere Variablenbehandlung, Glob-Abgleich, Regex-Abgleich und sauberere zusammengesetzte Bedingungen wünschst. Die Hauptgewohnheit ist einfach: Passe die Bedingungssyntax an die Shell an und setze Anführungszeichen bewusst.