Bash-Skripte zu debuggen heißt fast immer: mehr sichtbar machen. Es gibt keinen Debugger mit Breakpoints und Step-Funktion in der Standard-Bash, kein „Pause hier” und kein „inspect this variable” wie in einer IDE. Stattdessen sind die Werkzeuge der Wahl Trace-Modi, Syntax-Checks und externe Tools wie strace. Wer sie geschickt kombiniert, findet Fehler in Sekunden, statt blind echo-Zeilen einzustreuen.
Was Bash-Debugging schwierig macht
In einer typischen IDE setzt du einen Breakpoint, lässt das Programm dort anhalten, schaust dir Variablen an und gehst Schritt für Schritt weiter. In der Bash gibt es das alles in dieser Form nicht. Die Shell führt Befehle aus, sobald sie sie gelesen hat — eine eingebaute Pause-Funktion fehlt, ein Stepping-Mechanismus auch.
Die Bash bietet stattdessen drei andere Mittel:
| Mittel | Wirkung |
|---|---|
| Trace-Ausgabe | set -x druckt jeden Befehl vor der Ausführung — der wichtigste Hebel überhaupt |
| Syntax-Check | bash -n parst das Skript, ohne es laufen zu lassen |
| Externe Werkzeuge | strace, ltrace, bashdb ergänzen die Sicht auf Systemcalls und echte Breakpoints |
In der Praxis kommst du mit set -x und einem klaren Kopf erstaunlich weit. Die externen Tools sind die Reserve für Fälle, wo der Fehler unter der Bash-Ebene sitzt — etwa wenn ein Befehl kommentarlos mit Exit-Code 1 zurückkommt, ohne erkennbaren Grund.
set -x — der Trace-Modus (xtrace)
set -x (auch set -o xtrace) ist das Schweizer Taschenmesser des Bash-Debuggings. Bash druckt jeden Befehl, bevor er ausgeführt wird, mit einem + als Prefix nach stderr. Variablen sind in der Trace-Ausgabe bereits expandiert — du siehst also den tatsächlich laufenden Befehl, nicht den Quelltext.
name="Welt"
set -x
echo "Hallo $name"
set +x
echo "ohne Trace"+ echo 'Hallo Welt'
Hallo Welt
+ set +x
ohne Traceset +x (mit Plus) schaltet den Trace wieder aus. Das ist der typische Weg, um nur einen bestimmten Block zu debuggen, ohne die restliche Ausgabe mit Trace-Zeilen zu überfluten:
set -x
problematischer_block
set +xFür sehr lange Skripte ist das oft der einzige Weg, die Trace-Ausgabe lesbar zu halten. Alternativ schreibt man die Trace-Ausgabe gezielt in eine Datei: exec 2>trace.log setzt stderr global um, danach landet alles dort.
set -v — Verbose-Modus
set -v (auch set -o verbose) ist der weniger bekannte Bruder von -x. Er gibt jede Eingabezeile so aus, wie sie eingelesen wird — also vor der Expansion von Variablen, Glob-Patterns und Command-Substitutions.
name="Welt"
set -v
echo "Hallo $name"echo "Hallo $name"
Hallo WeltIm Vergleich zu set -x siehst du den Quelltext statt das expandierte Kommando. In den meisten Debug-Situationen will man genau das Gegenteil — wissen, was Bash am Ende tatsächlich ausführt. set -v ist deshalb selten die erste Wahl. Sinnvoll wird es, wenn du verstehen willst, wie Bash deine Eingabe parst (etwa bei verschachtelten Quotes oder Heredocs).
bash -n — Syntax-Check ohne Ausführung
bash -n script.sh (auch set -o noexec) liest und parst das Skript, führt aber keinen Befehl aus. Bash meldet Syntaxfehler — fehlende fi/done/esac, falsch gequotete Strings, kaputte Heredocs — bevor das Skript überhaupt startet. Das ist die billigste und sicherste Form der Validierung.
bash -n script.sh
echo "rc=$?"rc=0Liegt ein Syntaxfehler vor, gibt Bash eine Meldung mit Zeilennummer aus und endet mit Code ungleich 0. Genau diese Eigenschaft macht bash -n zum Pflichtprogramm in Pre-Commit-Hooks: Ein Skript mit Syntaxfehler darf gar nicht erst ins Repo. Die Prüfung dauert Millisekunden und kostet nichts.
bash -x — Skript mit Trace starten
Statt set -x ins Skript einzubauen, kannst du das gesamte Skript mit Trace starten, indem du es über bash -x aufrufst:
bash -x script.sh+ name=Welt
+ echo 'Hallo Welt'
Hallo WeltDer Vorteil: Du musst das Skript nicht editieren. Der Trace bleibt komplett extern — perfekt für fremde Skripte, für Tests in CI oder für eine schnelle Diagnose ohne Repo-Diff. Genauso funktionieren bash -v (Verbose) und bash -nv (Syntax-Check mit Verbose-Ausgabe).
Die Variante mit dem Shebang-Override geht auch: bash -x ./script.sh ignoriert den Shebang, weil Bash hier explizit aufgerufen wird. Damit kannst du gezielt eine andere Bash-Version benutzen, etwa /opt/homebrew/bin/bash -x script.sh auf macOS.
PS4 anpassen — Trace-Prefix erweitern
Das + vor jeder Trace-Zeile ist der Default des Prompt-String 4 (PS4). Du kannst ihn beliebig verändern, um mehr Kontext sichtbar zu machen — der Klassiker ist der Trace mit Datei und Zeilennummer:
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
name="Welt"
echo "Hallo $name"+ script.sh:3: name=Welt
+ script.sh:4: echo 'Hallo Welt'
Hallo WeltDamit weißt du sofort, welche Datei und welche Zeile den jeweiligen Befehl ausgelöst hat — Gold wert in Skripten, die andere Skripte sourcen oder Funktionen quer über mehrere Dateien aufrufen. Weitere nützliche Variablen für PS4:
| Variable | Inhalt |
|---|---|
${BASH_SOURCE} | Datei, in der der Befehl steht |
${LINENO} | Zeilennummer |
${FUNCNAME[0]} | Aktuelle Funktion (leer auf Top-Level) |
$$ | PID des aktuellen Skripts |
$SECONDS | Sekunden seit Skript-Start |
Ein vollwertiges Debug-PS4 sieht oft so aus: PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '. Die ##*/-Expansion kürzt den Pfad auf den Dateinamen und hält die Trace-Zeilen lesbar.
Praxis-Patterns
Die folgenden Muster decken typische Debug-Situationen ab. Jedes funktioniert eigenständig und lässt sich nach Bedarf kombinieren.
Trace-Sektion gezielt einschalten
set -x
problematischer_block
set +xSo bleibt die Trace-Ausgabe auf den verdächtigen Code beschränkt — der Rest des Skripts läuft normal weiter. Besonders praktisch in langen Skripten, wo eine vollständige Trace-Ausgabe schnell unübersichtlich wird.
Skript-weiter Trace ohne Quellcode-Änderung
bash -x script.sh
bash -xv script.sh 2>trace.logErste Form gibt den Trace direkt aus. Zweite Form leitet stderr (wo der Trace landet) in eine Datei um — so bleibt die normale Ausgabe sauber. xv kombiniert xtrace und verbose; meistens reicht -x.
Konditionaler Debug-Modus
[[ -n "${DEBUG:-}" ]] && set -xMit dieser Zeile am Skript-Anfang aktivierst du den Trace nur, wenn du das Skript mit DEBUG=1 ./script.sh startest. Im Normalbetrieb bleibt alles ruhig — keine Trace-Zeilen müllen die Ausgabe. Das ${DEBUG:-} schützt unter set -u davor, dass die Prüfung selbst abbricht, weil die Variable nicht gesetzt ist.
Eigene Log-Funktion mit Zeitstempel
log() { echo "[$(date +%T)] $*" >&2; }
log "Start: Backup wird vorbereitet"
log "Fehler beim Kopieren von $datei"Die Funktion schreibt mit Zeitstempel auf stderr — dadurch landen Logs nicht im stdout-Pipeline-Stream und stören keine Datenverarbeitung. Bei längeren Skripten lohnt sich eine Erweiterung um Loglevels (info, warn, error), bei sehr langen Läufen die Ausgabe in eine Datei via exec 2>>logfile.
strace für Systemcall-Ebene
strace -f -e openat ./script.sh 2>&1 | grep -v ENOENTstrace zeigt jeden Systemcall, den ein Prozess macht. Mit -e openat filterst du auf Datei-Zugriffe — extrem nützlich, wenn ein Tool eine Config-Datei nicht findet und du wissen willst, welchen Pfad es eigentlich aufruft. -f folgt Kindprozessen, grep -v ENOENT blendet die Nicht-gefunden-Versuche aus, falls du nur erfolgreiche Öffnungen sehen willst.
bashdb als externer Debugger
bashdb script.shbashdb ist ein vollwertiger Debugger im Stil von gdb — mit Breakpoints, Stepping, Watch-Expressions. In der Praxis spielt er kaum eine Rolle, weil die meisten Bash-Probleme mit set -x schneller zu finden sind. Für komplexe, mehrere hundert Zeilen lange Skripte, in denen man wirklich Schritt für Schritt eintauchen will, ist er aber das passende Werkzeug.
Häufige Stolperfallen
set -x druckt Passwörter und Secrets im Klartext
Sobald set -x aktiv ist, landet jeder expandierte Befehl mit allen Argumenten auf stderr — inklusive Passwörter, API-Tokens und Datenbank-URLs. In CI-Logs, die archiviert werden, ist das ein klassischer Secret-Leak. Vor sensiblen Bereichen set +x setzen, danach wieder set -x. Alternativ Secrets nie als Argument übergeben, sondern aus Dateien oder per stdin lesen, wo der Trace sie nicht mit ausspuckt.
bash -x mit set -e ist manchmal verwirrend
Wenn set -e aktiv ist, bricht das Skript beim ersten Fehlerbefehl ab — der Trace zeigt dir aber alle Befehle bis dahin, inklusive des fehlerhaften. In komplexen Pipes oder kurz hintereinander ausgeführten Subshells kann es schwerfallen, den Befehl zu identifizieren, der den Abbruch ausgelöst hat. Hier hilft ein PS4 mit Zeilennummer (PS4='+ ${LINENO}: ') und ein zusätzlicher ERR-Trap, der den abbrechenden Befehl explizit meldet.
PS4 mit $(...) wird pro Zeile neu ausgewertet
Ein PS4='+ $(date +%T) ' sieht hilfreich aus — bedeutet aber, dass Bash bei jedem Trace-Befehl date startet. Bei einem Skript mit zehntausenden Zeilen Trace ist das ein massiver Performance-Killer und produziert Tausende fork-Calls. Für Zeitstempel ist $SECONDS (Skript-Laufzeit in Sekunden) deutlich billiger, weil es ein Built-in ist und keinen Subprozess startet.
Trace in Subshells geht verloren
set -x wird an Subshells vererbt — set +x allerdings nicht zurück: Wenn du innerhalb von (...) den Trace ausschaltest, gilt das nur in der Subshell. Auch der Übergang in eine Pipe oder $(...) kann je nach Bash-Version dazu führen, dass der Trace dort nicht erscheint. Lösung: set -x oben ins Skript und nicht versuchen, das Verhalten subshell-genau zu steuern. Wer Subshell-Code separat tracen will, setzt set -x innerhalb der Subshell.
strace vs. ltrace zeigen unterschiedliche Ebenen
strace verfolgt Systemcalls — also Aufrufe an den Kernel (open, read, write, execve). ltrace verfolgt dagegen Library-Calls — also Aufrufe an dynamisch gelinkte Bibliotheken (malloc, printf, strlen). Wer ein „Datei nicht gefunden”-Problem hat, will strace. Wer wissen will, welche glibc-Funktion ein Programm aufruft, nimmt ltrace. Statisch gelinkte Programme zeigen in ltrace nichts — dort hilft nur strace.
Weiterführende Ressourcen
Externe Quellen
- Bash-Manpage: SHELL BUILTIN COMMANDS — set — Offizielle Doku zu
set -x,set -v,set -n - GNU Bash-Handbuch: The Set Builtin — Ausführliche Beschreibung aller Set-Optionen mit Beispielen
- bashdb-Projekt (SourceForge) — Externer Debugger mit Breakpoints und Stepping
- strace.io — Offizielle Seite zum Systemcall-Tracer
- Greg’s Wiki: BashFAQ/035 — Debugging — Praxis-Tipps zum Skript-Debugging
Verwandte Artikel
- Shell-Scripting ShellCheck — Statischer Linter, der über
bash -nweit hinausgeht - Shell-Scripting Fehlerbehandlung —
set -e,trapund robuste Cleanup-Patterns - Shell-Scripting Skript-Grundlagen — Shebang, Header und Ausführungsmodi
- Shell-Scripting Exit-Codes —
$?,pipefailund Status-Auswertung - Shell-Scripting Funktionen — Funktionen,
returnund lokale Variablen