Schleifen sind das Arbeitstier jedes Shell-Skripts. Bash kennt vier Konstrukte — for, while, until und select — plus die Steuerbefehle break und continue. Wer die richtige Form für den jeweiligen Job kennt und die Subtilitäten von Word-Splitting, Globbing und Subshells beherrscht, schreibt Skripte, die nicht beim ersten Sonderzeichen im Dateinamen umfallen.

Die drei Formen der for-Schleife

Bash kennt drei syntaktische Varianten der for-Schleife. Jede hat ihren eigenen Einsatzbereich.

Die klassische Word-Iteration läuft über eine Liste von Werten, getrennt durch Whitespace:

Bash for in liste
for name in alice bob carol; do
    echo "Hallo, $name"
done

Die C-Stil-Form mit doppelten Klammern erlaubt arithmetische Ausdruecke und ist die richtige Wahl, wenn du einen Zähler brauchst:

Bash for ((arithmetisch))
for ((i=0; i<10; i++)); do
    echo "Iteration $i"
done

Die Glob-Iteration kombiniert for in mit Wildcards. Bash expandiert das Muster, bevor die Schleife startet:

Bash for über Glob
for f in *.txt; do
    echo "Verarbeite $f"
done

Wichtig: Die Variable in der Schleife sollte immer in doppelten Anführungszeichen stehen ("$f"), sobald du sie an einen Befehl weitergibst — sonst zerlegt Bash Dateinamen mit Leerzeichen.

Über Dateien iterieren — sicher

Der mit Abstand häufigste Fehler in Bash-Skripten ist for line in $(cat file). Die Konstruktion sieht harmlos aus, ist aber gleich mehrfach kaputt: Bash zerlegt den Output an Whitespace (Word-Splitting) und expandiert anschliessend Glob-Pattern. Eine Zeile mit zwei Woertern wird zu zwei Iterationen, ein * in der Datei matcht plötzlich Dateinamen aus dem aktuellen Verzeichnis.

Das Standard-Pattern für zeilenweises Lesen ist while read:

Bash while read — sauber
while IFS= read -r line; do
    echo "Zeile: $line"
done < datei.txt

IFS= deaktiviert das Trimmen von fuehrenden und nachfolgenden Whitespace-Zeichen, -r verhindert, dass Backslashes interpretiert werden. Beides zusammen liefert die Zeile genau so, wie sie in der Datei steht.

Wenn Dateinamen Sonderzeichen enthalten können — Newlines, Tabs, was auch immer — kombinierst du find -print0 mit NUL-getrenntem read:

Bash NUL-getrennt mit find
find . -type f -name "*.log" -print0 | while IFS= read -r -d '' file; do
    echo "Datei: $file"
done

Der Trenner \0 taucht in keinem gueltigen Dateinamen auf, deshalb ist diese Variante absolut sicher.

Über Befehls-Output iterieren

Der vorige Abschnitt zeigt: Pipes in while haben einen Haken — die Schleife läuft in einer Subshell, Variablen sind nach dem Loop weg. Die saubere Lösung ist Process Substitution mit < <(cmd):

Bash while mit Process Substitution
count=0
while IFS= read -r line; do
    count=$((count + 1))
done < <(grep "ERROR" /var/log/syslog)
echo "Treffer: $count"

Hier läuft grep zwar in einem eigenen Prozess, der while-Block aber in der aktuellen Shell — count ist nach der Schleife weiterhin sichtbar. Mehr dazu im Artikel zu Prozess-Substitution.

while — laufe, solange wahr

Eine while-Schleife prüft die Bedingung am Anfang jeder Iteration und läuft, solange diese wahr ist (Exit-Code 0).

Bash while mit Zähler
i=0
while [ $i -lt 5 ]; do
    echo "i=$i"
    i=$((i + 1))
done

Ein typisches Idiom ist die Endlosschleife mit explizitem break als Ausstieg:

Bash while true mit break
while true; do
    read -p "Befehl (q zum Beenden): " cmd
    [ "$cmd" = "q" ] && break
    echo "Du gibst ein: $cmd"
done

Das Muster ist immer dann passend, wenn die Abbruchbedingung mitten im Schleifenkoerper steht und nicht elegant in einen Ausdruck am Anfang passt.

until — laufe, bis wahr

until ist das logische Gegenstueck zu while: Die Schleife läuft, bis die Bedingung wahr wird. Im Alltag selten gebraucht, aber idiomatic, wenn du auf einen Zustand wartest:

Bash warten bis Service erreichbar
until curl -sf http://localhost:8080/health > /dev/null; do
    echo "Warte auf Service..."
    sleep 1
done
echo "Service ist hoch."

Solange curl einen Fehlercode liefert, läuft die Schleife. Sobald der Health-Check antwortet, bricht sie ab. Das ist sauberer als while ! cmd, weil die Absicht direkt im Konstrukt steht.

select — interaktive Menüs

select baut aus einer Liste ein nummeriertes Menü, liest die Eingabe und setzt die Variable auf den gewählten Eintrag. Selten gebraucht, aber elegant für kleine CLIs:

Bash select-Menü
select choice in "Status anzeigen" "Neustart" "Beenden"; do
    case "$choice" in
        "Status anzeigen") systemctl status nginx ;;
        "Neustart") sudo systemctl restart nginx ;;
        "Beenden") break ;;
        *) echo "Ungueltige Auswahl" ;;
    esac
done

Bash zeigt automatisch die nummerierten Optionen an und liest mit dem Prompt aus $PS3 (Default: #? ). Ohne break läuft select endlos und fragt nach jeder Aktion erneut.

break und continue

break verlässt die aktuelle Schleife sofort, continue springt zur nächsten Iteration. Beide nehmen optional einen Level-Parameter, mit dem du verschachtelte Schleifen verlassen kannst:

Bash break mit Level
for i in 1 2 3; do
    for j in a b c; do
        if [ "$j" = "b" ]; then
            break 2
        fi
        echo "$i-$j"
    done
done

break 2 verlässt sowohl die innere als auch die äußere Schleife. Ohne Level (oder mit break 1) wäre nur die innere betroffen, und die äußere würde mit i=2 weiterlaufen.

continue funktioniert analog: continue 2 überspringt den Rest beider Schleifenkoerper und startet die nächste Iteration der äußeren Schleife.

Praxis-Patterns

Sechs Schleifen-Patterns, die in fast jedem Skript vorkommen — jeweils mit kurzer Erläuterung.

Alle Dateien in einem Ordner verarbeiten — die Glob-Iteration übernimmt Quoting und Sortierung; aktiviere shopt -s nullglob, falls der Ordner leer sein könnte:

Bash Files in Ordner
shopt -s nullglob
for f in dir/*; do
    echo "Datei: $f"
done

Range mit Brace-Expansion{1..10} expandiert zu zehn einzelnen Argumenten, ohne externe Tools wie seq:

Bash Range mit Brace
for i in {1..10}; do
    echo "Schritt $i"
done

Zeilenweise lesen — der Standard für Log- und Konfig-Dateien; IFS= und -r sind Pflicht:

Bash while read aus Datei
while IFS= read -r line; do
    [[ "$line" =~ ^# ]] && continue
    echo "Konfig-Zeile: $line"
done < /etc/myapp.conf

Mit Index über Array iterieren — wenn du nicht nur den Wert, sondern auch die Position brauchst:

Bash for mit Index
arr=(rot gruen blau)
for ((i=0; i<${#arr[@]}; i++)); do
    echo "$i: ${arr[i]}"
done

Wartender Loop — perfekt für Healthchecks, Boot-Scripte und Deployment-Pipelines:

Bash until pgrep
until pgrep -f myservice > /dev/null; do
    sleep 1
done
echo "Service laeuft."

Parallele Verarbeitung mit xargs -P — wenn die Schleife seriell zu langsam ist, lohnt der Sprung zu xargs mit -P für paralleles Ausführen. Details im Artikel zur Textverarbeitung:

Bash parallel statt for
find . -name "*.jpg" -print0 | xargs -0 -P 4 -I {} convert {} -resize 800x {}

Stolperfallen mit Schleifen

for x in $(cmd) zerlegt Output unkontrolliert

Das Muster for line in $(cat file) (oder beliebig andere Command Substitution) sieht intuitiv aus, ist aber doppelt kaputt. Erstens spaltet Bash den Output an jedem Zeichen aus IFS (Default: Space, Tab, Newline) — eine Zeile mit zwei Woertern wird zu zwei Iterationen. Zweitens expandiert die Shell anschliessend Glob-Pattern: Steht ein &#42; in der Datei, matcht es plötzlich Dateinamen im aktuellen Verzeichnis. Die korrekte Form ist immer while IFS= read -r line; do ... done < file.

while in einer Pipe läuft in einer Subshell

cmd | while read line; do counter=$((counter+1)); done zählt zwar fleissig hoch, aber counter ist nach der Schleife genauso 0 wie davor. Grund: Jede Stufe einer Pipe läuft in einer eigenen Subshell, und Variablen-Zuweisungen darin sind nach dem Ende verloren. Lösungen: Process Substitution mit done < <(cmd), oder in Bash 4.2+ shopt -s lastpipe (erfordert ausserdem set +m). Das gleiche Problem trifft break und continue — sie wirken nur in der Subshell, nicht in einer eventuellen äußeren Schleife.

Unquoted Variable in der for-Liste bricht

for f in $files zerlegt den Inhalt von $files an Whitespace und expandiert Globs. Wenn $files mehrere durch Whitespace getrennte Pfade enthaelt, ist das vielleicht beabsichtigt — meistens aber nicht. Für mehrere Werte sind Bash-Arrays die richtige Wahl: files=(a.txt "b mit space.txt" c.txt); for f in "$&#123;files[@]&#125;"; do .... Beachte das obligatorische "$&#123;files[@]&#125;" mit Anführungszeichen — ohne sie passiert wieder Word-Splitting.

for f in *.txt liefert das Pattern, wenn nichts matcht

Standardmäßig gibt Bash bei einem Glob ohne Treffer das Pattern selbst zurück. for f in *.txt; do echo "$f"; done gibt in einem leeren Verzeichnis genau einmal &#42;.txt aus — als Literal. Skripte, die das nicht beachten, behandeln den Pattern-String als Dateinamen und scheitern dann an cat: '&#42;.txt': No such file or directory. Fix: shopt -s nullglob setzen, dann liefert ein leerer Glob eine leere Liste — und die Schleife läuft schlicht keine Iteration.

Ohne IFS= und -r liest read kaputt

read line ohne IFS= schneidet fuehrende und nachfolgende Whitespace-Zeichen ab — Einrueckung in Konfig-Dateien geht verloren. Ohne -r interpretiert read Backslash-Sequenzen: Aus pfad\file wird pfadfile. Für beliebige Dateiinhalte ist while IFS= read -r line Pflicht. Bonus-Falle: Wenn die letzte Zeile keinen Newline am Ende hat, liefert read Exit-Code 1 — die Schleife überspringt sie. Workaround: while IFS= read -r line || [ -n "$line" ]; do ....

break und continue funktionieren nicht über Funktionen

Wenn du eine Schleife in einer Funktion verschachtelst, erwarten viele, dass break aus der Funktion heraus die aufrufende Schleife verlässt. Tut es nicht: break operiert immer auf der lexikalisch innersten Schleife des aktuellen Skripts. Aus einer Funktion kannst du nur die Funktion mit return beenden — die außen laufende Schleife geht trotzdem weiter. Wer eine Schleife konditional von innen aus einer Funktion abbrechen will, gibt einen Exit-Code zurück und prüft den im Caller mit && oder ||.

Weiterfuehrende Ressourcen

Externe Quellen

  • Bedingungenif, test und [[ ... ]] als Schleifen-Kondition
  • Parameter — Positionsparameter und $@ in Schleifen verarbeiten
  • Prozess-Substitution< <(cmd) als Subshell-Falle-Vermeidung
  • Arrays — Indexierte und assoziative Arrays in Schleifen
  • Fehlerbehandlungset -e, set -o pipefail und trap in Loops
/ Weiter

Zurück zu Shell-Scripting

Zur Übersicht