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:
for name in alice bob carol; do
echo "Hallo, $name"
doneDie C-Stil-Form mit doppelten Klammern erlaubt arithmetische Ausdruecke und ist die richtige Wahl, wenn du einen Zähler brauchst:
for ((i=0; i<10; i++)); do
echo "Iteration $i"
doneDie Glob-Iteration kombiniert for in mit Wildcards. Bash expandiert das Muster, bevor die Schleife startet:
for f in *.txt; do
echo "Verarbeite $f"
doneWichtig: 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:
while IFS= read -r line; do
echo "Zeile: $line"
done < datei.txtIFS= 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:
find . -type f -name "*.log" -print0 | while IFS= read -r -d '' file; do
echo "Datei: $file"
doneDer 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):
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).
i=0
while [ $i -lt 5 ]; do
echo "i=$i"
i=$((i + 1))
doneEin typisches Idiom ist die Endlosschleife mit explizitem break als Ausstieg:
while true; do
read -p "Befehl (q zum Beenden): " cmd
[ "$cmd" = "q" ] && break
echo "Du gibst ein: $cmd"
doneDas 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:
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:
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
doneBash 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:
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
donebreak 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:
shopt -s nullglob
for f in dir/*; do
echo "Datei: $f"
doneRange mit Brace-Expansion — {1..10} expandiert zu zehn einzelnen Argumenten, ohne externe Tools wie seq:
for i in {1..10}; do
echo "Schritt $i"
doneZeilenweise lesen — der Standard für Log- und Konfig-Dateien; IFS= und -r sind Pflicht:
while IFS= read -r line; do
[[ "$line" =~ ^# ]] && continue
echo "Konfig-Zeile: $line"
done < /etc/myapp.confMit Index über Array iterieren — wenn du nicht nur den Wert, sondern auch die Position brauchst:
arr=(rot gruen blau)
for ((i=0; i<${#arr[@]}; i++)); do
echo "$i: ${arr[i]}"
doneWartender Loop — perfekt für Healthchecks, Boot-Scripte und Deployment-Pipelines:
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:
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 * 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 "${files[@]}"; do .... Beachte das obligatorische "${files[@]}" 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 *.txt aus — als Literal. Skripte, die das nicht beachten, behandeln den Pattern-String als Dateinamen und scheitern dann an cat: '*.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
- Bash-Manpage: Looping Constructs — Offizielle Referenz für
for,while,untilundselect - GNU Bash-Handbuch: Looping Constructs — Vollstaendige Beschreibung mit Beispielen
- BashFAQ: I want to process a list of files — Der Klassiker zum sicheren Iterieren über Dateien
- BashPitfalls — Gesammelte Stolperfallen mit Schleifen-Bezug
Verwandte Artikel
- Bedingungen —
if,testund[[ ... ]]als Schleifen-Kondition - Parameter — Positionsparameter und
$@in Schleifen verarbeiten - Prozess-Substitution —
< <(cmd)als Subshell-Falle-Vermeidung - Arrays — Indexierte und assoziative Arrays in Schleifen
- Fehlerbehandlung —
set -e,set -o pipefailundtrapin Loops