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:

MittelWirkung
Trace-Ausgabeset -x druckt jeden Befehl vor der Ausführung — der wichtigste Hebel überhaupt
Syntax-Checkbash -n parst das Skript, ohne es laufen zu lassen
Externe Werkzeugestrace, 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.

Bash set -x ein- und ausschalten
name="Welt"
set -x
echo "Hallo $name"
set +x
echo "ohne Trace"
Output
+ echo 'Hallo Welt'
Hallo Welt
+ set +x
ohne Trace

set +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:

Bash Nur einen Block tracen
set -x
problematischer_block
set +x

Fü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.

Bash verbose vs. xtrace
name="Welt"
set -v
echo "Hallo $name"
Output
echo "Hallo $name"
Hallo Welt

Im 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 Syntax-Check eines Skripts
bash -n script.sh
echo "rc=$?"
Output
rc=0

Liegt 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 Trace ohne Skript-Änderung
bash -x script.sh
Output
+ name=Welt
+ echo 'Hallo Welt'
Hallo Welt

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

Bash Trace mit Source-Info
PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
name="Welt"
echo "Hallo $name"
Output
+ script.sh:3: name=Welt
+ script.sh:4: echo 'Hallo Welt'
Hallo Welt

Damit 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:

VariableInhalt
${BASH_SOURCE}Datei, in der der Befehl steht
${LINENO}Zeilennummer
${FUNCNAME[0]}Aktuelle Funktion (leer auf Top-Level)
$$PID des aktuellen Skripts
$SECONDSSekunden 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

Bash Nur einen Block tracen
set -x
problematischer_block
set +x

So 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 Trace extern aktivieren
bash -x script.sh
bash -xv script.sh 2>trace.log

Erste 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

Bash Debug per Umgebungsvariable
[[ -n "${DEBUG:-}" ]] && set -x

Mit 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

Bash Strukturiertes Logging nach stderr
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

Bash File-Access nachverfolgen
strace -f -e openat ./script.sh 2>&1 | grep -v ENOENT

strace 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

Bash Echte Breakpoints mit bashdb
bashdb script.sh

bashdb 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

/ Weiter

Zurück zu Shell-Scripting

Zur Übersicht