0–255, POSIX-Bedeutungen, $?, set -e und Pipe-Exit-Codes
Exit-Codes sind die einzige strukturierte Schnittstelle zwischen Bash-Skripten. Wer versteht, was 0–255 bedeuten, wie $? funktioniert, wann set -e nicht auslöst und wie pipefail Pipe-Exit-Codes rettet, schreibt Skripte, die zuverlässig miteinander kommunizieren – statt Fehler still zu übersehen.
Inhaltsverzeichnis
- 1. Was Exit-Codes wirklich sind
- 2. POSIX-Exit-Code-Bedeutungen: 0–255 im Detail
- 3. $? auswerten: die richtige und die falsche Art
- 4. set -e: Verhalten, Grenzen und Fallen
- 5. Pipe-Exit-Codes und pipefail
- 6. PIPESTATUS: jeden Schritt in einer Pipe auswerten
- 7. Eigene Exit-Codes als API definieren
- 8. Exit-Codes in Subshells, Funktionen und Hintergrundprozessen
- 9. Exit-Code-Verhalten im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Exit-Codes wirklich sind
Exit-Codes sind ganzzahlige Werte zwischen 0 und 255, die ein Prozess beim Beenden an seinen Elternprozess zurückgibt. In Bash ist dieser Wert über die spezielle Variable $? nach jedem Befehl verfügbar. Der Wert 0 bedeutet Erfolg – jeder andere Wert signalisiert einen Fehler oder eine bestimmte Bedeutung, abhängig vom Programm. Diese einfache Konvention ist die fundamentale Kommunikationsschnittstelle zwischen Prozessen in Unix-Systemen und damit zwischen Bash-Skripten.
Das Konzept der Exit-Codes als API zu begreifen bedeutet: Wenn ein Bash-Skript ein anderes Skript oder Programm aufruft, ist der Exit-Code des aufgerufenen Prozesses der einzige standardisierte Rückgabekanal. Natürlich gibt es auch stdout und stderr – aber während diese freitext-basiert und programm-spezifisch sind, sind Exit-Codes ein universeller Standard. Jedes POSIX-konforme Programm, jedes Shell-Builtin, jedes Bash-Skript kommuniziert über denselben Mechanismus. Wer diesen Mechanismus versteht und konsequent nutzt, baut Skripte, die verlässlich miteinander interagieren.
In der Praxis sieht man häufig Bash-Skripte, die Exit-Codes ignorieren: Ein Befehl wird aufgerufen, das Ergebnis wird nicht geprüft, das Skript läuft weiter. Das ist equivalent zu einem API-Aufruf, bei dem die HTTP-Statusantwort ignoriert wird. Die Folgen sind dieselben: Das Skript führt möglicherweise Folgeschritte aus, die auf einer falschen Ausgabe oder einem nicht vorhandenen Ergebnis aufbauen, ohne dass ein Fehler signalisiert wird.
2. POSIX-Exit-Code-Bedeutungen: 0–255 im Detail
POSIX definiert für den Wertebereich der Exit-Codes einen groben Standard. 0 bedeutet Erfolg. 1 ist der generische Fehlercode – die meisten Unix-Programme verwenden ihn für allgemeine Fehler. 2 wird oft für falsche Verwendung oder ungültige Argumente genutzt. Der Bereich 3–125 ist für programmspezifische Exit-Codes frei. Der Bereich 126–127 ist für Shell-spezifische Fehlermeldungen reserviert: 126 bedeutet, dass ein Programm gefunden, aber nicht ausführbar war. 127 bedeutet, dass ein Programm nicht gefunden wurde. 128 + N ist das Muster für signalbedingte Beendigungen: Ein durch Signal 9 (SIGKILL) beendeter Prozess gibt 137 zurück (128 + 9). Ein durch Signal 15 (SIGTERM) beendeter Prozess gibt 143 zurück (128 + 15).
Bash selbst setzt Exit-Codes für spezifische Situationen: 130 für Unterbrechung durch CTRL+C (SIGINT, Signal 2), 141 für Broken Pipe (SIGPIPE, Signal 13). Das Verstehen dieser Bedeutungen ist entscheidend für die korrekte Behandlung von Signalen in Trap-Handlern. Ein Skript, das mit trap 'cleanup; exit 130' INT SIGINT abfängt, gibt den korrekten Exit-Code für Ctrl+C weiter und ermöglicht dem Aufrufer, zwischen normalem Fehler und benutzerinitiiertem Abbruch zu unterscheiden.
#!/usr/bin/env bash
# exit-code-demo.sh — Demonstrate POSIX exit code conventions
set -euo pipefail
# Define meaningful exit codes as named constants
readonly EXIT_SUCCESS=0
readonly EXIT_GENERAL_ERROR=1
readonly EXIT_USAGE_ERROR=2
readonly EXIT_CONFIG_MISSING=3
readonly EXIT_NETWORK_UNREACHABLE=4
readonly EXIT_PERMISSION_DENIED=5
readonly EXIT_TIMEOUT=6
readonly EXIT_LOCK_HELD=7
usage() {
echo "Usage: $(basename "$0") <command> [args...]" >&2
exit "$EXIT_USAGE_ERROR"
}
validate_config() {
local config_file="$1"
if [[ ! -f "$config_file" ]]; then
echo "[ERROR] Config file not found: $config_file" >&2
exit "$EXIT_CONFIG_MISSING"
fi
if [[ ! -r "$config_file" ]]; then
echo "[ERROR] Config file not readable: $config_file" >&2
exit "$EXIT_PERMISSION_DENIED"
fi
}
# Caller can distinguish between error types
[[ $# -lt 1 ]] && usage
validate_config "${CONFIG_FILE:-/etc/app/config.yml}"
# Check exit code of external command
if ! curl -sf --max-time 10 "https://api.mironsoft.de/health" >/dev/null 2>&1; then
last_exit=$?
if [[ $last_exit -eq 28 ]]; then # curl timeout = 28
echo "[ERROR] API health check timed out" >&2
exit "$EXIT_TIMEOUT"
else
echo "[ERROR] API health check failed (curl: $last_exit)" >&2
exit "$EXIT_NETWORK_UNREACHABLE"
fi
fi
echo "[OK] All checks passed"
exit "$EXIT_SUCCESS"
3. $? auswerten: die richtige und die falsche Art
Die spezielle Variable $? enthält den Exit-Code des zuletzt ausgeführten Befehls. Die wichtigste Eigenschaft von $?: Sie wird sofort durch den nächsten ausgeführten Befehl überschrieben. Wer also $? nach mehreren Befehlen auswerten will, muss sie in einer normalen Variable sichern: exit_code=$?. Das sofortige Sichern von $? nach einem Befehl ist die einzig korrekte Art, den Exit-Code eines bestimmten Befehls zu verwerten.
Ein häufiger Fehler: befehl; if [ $? -ne 0 ] ist anfällig, weil zwischen dem Befehl und dem if-Check andere Befehle ausgeführt werden könnten (z.B. durch Redirections oder Subshell-Aufrufe, die $? überschreiben). Der robustere Ansatz ist entweder das direkte Testen des Befehls in der if-Bedingung (if ! befehl; then) oder das sofortige Speichern: befehl; rc=$?; if [[ $rc -ne 0 ]]. Der direkteste Weg ist die Verwendung von if befehl; then – das ist idiomatisches Bash und lässt $? im Hintergrund.
4. set -e: Verhalten, Grenzen und Fallen
set -e ist eine der wichtigsten Sicherheitsmechanismen bei der Verwendung von Exit-Codes in Bash-Skripten. Es weist Bash an, das Skript sofort zu beenden, wenn ein Befehl einen Nicht-Null-Exit-Code zurückgibt. Ohne set -e ignoriert Bash fehlgeschlagene Befehle standardmäßig und setzt die Ausführung fort – ein Verhalten, das in interaktiven Shells sinnvoll ist, in Automatisierungsskripten aber Fehler verdeckt.
Die wichtigste Einschränkung: set -e löst in Bedingungskontexten nicht aus. Wenn ein Befehl Teil einer if-Bedingung ist (if fehlschlagender_befehl; then), nach || steht (fehlschlagender_befehl || fallback) oder nach && kommt (erfolg && fehlschlagender_befehl), wird der Nicht-Null-Exit-Code nicht als Abbruchbedingung behandelt. Das ist beabsichtigt – in diesen Kontexten ist der Exit-Code ein Bedingungsergebnis, kein Fehler. Wer diese Stellen dennoch absichern will, muss explizit || { echo "Fehler"; exit 1; } schreiben.
#!/usr/bin/env bash
# set-e-behavior.sh — Understanding set -e edge cases
set -euo pipefail
# CASE 1: set -e triggers — script exits immediately
# false # Uncomment to see: script stops here with exit code 1
# CASE 2: set -e does NOT trigger in if-context
if false; then
echo "unreachable"
fi
echo "After if: still running (exit code was NOT treated as error)"
# CASE 3: set -e does NOT trigger after ||
false || echo "Fallback executed — false's exit code was consumed by ||"
# CASE 4: set -e does NOT trigger after &&
true && false || echo "Right side of && failed — consumed by ||"
# CASE 5: Correctly saving exit code before set -e can fire
run_and_check() {
local cmd="$1"
local rc=0
# Subshell so set -e doesn't kill parent
( eval "$cmd" ) && rc=$? || rc=$?
if [[ $rc -ne 0 ]]; then
echo "[WARN] Command '$cmd' exited with code $rc"
fi
return "$rc"
}
run_and_check "ls /nonexistent" || true
# CASE 6: Explicit exit code propagation
deploy_step() {
local step_name="$1"
shift
if ! "$@"; then
echo "[FAIL] Step '$step_name' failed with exit code $?" >&2
return 1
fi
echo "[OK] Step '$step_name' succeeded"
}
deploy_step "health-check" curl -sf http://localhost/health
5. Pipe-Exit-Codes und pipefail
Pipes sind eine der mächtigsten Funktionen von Bash, haben aber eine kritische Eigenschaft bei Exit-Codes: Ohne set -o pipefail ist der Exit-Code einer Pipeline immer der Exit-Code des letzten Befehls in der Pipe. Ein Fehler in einem frühen Schritt der Pipeline wird vollständig verdeckt, solange der letzte Befehl erfolgreich ist. Das klassische Beispiel: failing_command | grep "pattern" gibt Exit-Code 0 zurück, wenn grep das Muster findet oder wenn grep keine Ausgabe vom fehlgeschlagenen Befehl zu verarbeiten hatte.
Mit set -o pipefail gibt eine Pipeline den Exit-Code des letzten Befehls mit Nicht-Null-Exit-Code zurück. Wenn alle Befehle in der Pipe erfolgreich sind, ist der Exit-Code der Pipeline 0. Das Zusammenspiel von set -e und set -o pipefail stellt sicher, dass kein fehlgeschlagener Befehl in einer Pipeline verborgen bleibt. Die Kombination set -euo pipefail deckt damit die häufigsten Quellen von unerkannten Fehlern in Bash-Skripten ab.
6. PIPESTATUS: jeden Schritt in einer Pipe auswerten
PIPESTATUS ist ein Bash-Array, das die Exit-Codes aller Befehle in der letzten Pipeline enthält. Nach cmd1 | cmd2 | cmd3 enthält ${PIPESTATUS[0]} den Exit-Code von cmd1, ${PIPESTATUS[1]} den von cmd2 und ${PIPESTATUS[2]} den von cmd3. Das erlaubt granulare Fehlerdiagnose: Welcher Schritt in einer komplexen Pipe-Kette ist gescheitert? Diese Information ist bei pipefail allein nicht verfügbar, weil nur der Code des zuletzt fehlgeschlagenen Befehls zurückgegeben wird.
PIPESTATUS muss sofort nach der Pipe gesichert werden, da es durch den nächsten Befehl (auch durch ein einfaches echo) überschrieben wird. Das Muster: cmd1 | cmd2; pipe_codes=("${PIPESTATUS[@]}") sichert alle Exit-Codes der Pipe-Stufen. Anschließend kann jeder Code einzeln ausgewertet werden. Dieses Muster ist besonders in Build-Pipelines wertvoll, wo man wissen muss, ob ein Compilerfehler, ein Linting-Fehler oder ein Ausgabe-Filterschritt fehlgeschlagen ist.
#!/usr/bin/env bash
# pipestatus-demo.sh — Granular pipe exit code analysis
set -uo pipefail
check_pipeline() {
local description="$1"
shift
# Run pipeline and capture PIPESTATUS immediately
eval "$@" || true
local -a statuses=("${PIPESTATUS[@]}")
local all_ok=true
for i in "${!statuses[@]}"; do
if [[ "${statuses[$i]}" -ne 0 ]]; then
echo "[FAIL] Pipeline '$description': step $((i+1)) exited with ${statuses[$i]}" >&2
all_ok=false
fi
done
$all_ok || return 1
echo "[OK] Pipeline '$description' succeeded"
}
# Example: multi-step pipeline with individual exit code tracking
process_logs() {
local logfile="$1"
local pattern="$2"
local output="$3"
grep -E "$pattern" "$logfile" \
| sort -u \
| awk '{print NR": "$0}' \
> "$output"
local -a statuses=("${PIPESTATUS[@]}")
# grep exits 1 if no match (not an error per se)
if [[ "${statuses[0]}" -eq 1 ]]; then
echo "[INFO] No matches found for pattern '$pattern'" >&2
return 0
fi
# sort or awk failing is a real error
for i in 1 2; do
if [[ "${statuses[$i]}" -ne 0 ]]; then
echo "[FAIL] Step $((i+1)) in log processing failed with code ${statuses[$i]}" >&2
return 1
fi
done
echo "[OK] Processed $(wc -l < "$output") matching lines"
}
process_logs "/var/log/syslog" "ERROR|CRITICAL" "/tmp/errors.txt"
7. Eigene Exit-Codes als API definieren
Das Definieren eigener Exit-Codes als dokumentierte API ist ein Qualitätsmerkmal professioneller Shell-Skripte. Anstatt pauschal Exit-Code 1 für alle Fehlertypen zu verwenden, reserviert man spezifische Codes für verschiedene Fehlerzustände: 2 für Konfigurationsfehler, 3 für Netzwerkfehler, 4 für Timeout, 5 für fehlende Abhängigkeiten. Aufrufer – andere Skripte, Cron-Jobs, CI-Pipelines – können den Exit-Code auswerten und entsprechend reagieren: Bei Timeout automatisch erneut versuchen, bei Konfigurationsfehlern Alarm schlagen, bei Netzwerkfehlern eine Wartezeit einlegen.
Diese Exit-Code-API sollte in einem Header-Kommentar des Skripts dokumentiert werden und als readonly-Konstanten definiert sein. Das verhindert versehentliches Überschreiben und macht die möglichen Rückgabewerte sofort sichtbar. Wenn mehrere Skripte zusammenarbeiten, empfiehlt sich eine gemeinsame Bibliotheksdatei exit-codes.sh, die von allen Skripten per source eingebunden wird – so sind die Exit-Codes konsistent über alle Skripte eines Projekts.
8. Exit-Codes in Subshells, Funktionen und Hintergrundprozessen
Bash-Funktionen geben ihren letzten Befehlsstatus als Exit-Code zurück, oder den explizit per return N gesetzten Wert. Wichtig: return kann in Funktionen nur Werte von 0–255 zurückgeben – keine Strings, keine Objekte. Wenn eine Funktion einen komplexen Status kommunizieren muss, ist die Ausgabe nach stdout oder das Setzen einer globalen Variable der übliche Weg. Der Exit-Code kommuniziert nur Erfolg (0) oder einen von mehreren definierten Fehlerzuständen.
Bei Hintergrundprozessen (befehl &) ist der Exit-Code nicht sofort über $? verfügbar. Er wird erst nach dem wait PID-Aufruf gesetzt. Ein häufiger Fehler: job &; pid=$!; echo "started"; wait $pid; if [[ $? -ne 0 ]] – das echo zwischen & und wait überschreibt $?, aber das ist hier korrekt, weil $? erst nach wait ausgewertet wird. Werden mehrere Hintergrundprozesse gestartet, muss jede PID einzeln mit wait abgewartet und ihr Exit-Code gesichert werden.
9. Exit-Code-Verhalten im direkten Vergleich
Das Verhalten von Exit-Codes unterscheidet sich in verschiedenen Bash-Kontexten erheblich. Diese Tabelle zeigt die wichtigsten Unterschiede, die häufig zu Missverständnissen führen.
| Kontext | Nicht-Null Exit-Code | set -e Reaktion | Empfehlung |
|---|---|---|---|
| Normaler Befehl | false, ls /nofile |
Skript bricht ab | Gewünscht – Standardverhalten |
| if-Bedingung | if false; then |
Kein Abbruch | Explizit mit || exit 1 absichern wenn nötig |
| Nach || Operator | false || echo fallback |
Kein Abbruch | Bewusst nutzen für kontrollierte Fallbacks |
| Pipe ohne pipefail | false | grep x |
Kein Abbruch (verdeckt!) | Immer set -o pipefail nutzen |
| Hintergrundprozess | false & |
Kein Abbruch | wait $! und Exit-Code manuell prüfen |
Die Tabelle zeigt, warum Exit-Codes als API konsequent verstanden werden müssen: In jedem Kontext verhält sich Bash anders. Die wichtigste Regel: set -euo pipefail deckt normale Befehle und Pipes ab, aber Bedingungskontexte und Hintergrundprozesse erfordern immer explizite Behandlung. Wer das verinnerlicht, vermeidet die häufigsten Quellen von stillen Fehlern in Bash-Skripten.
Mironsoft
Shell-Automatisierung, Fehlerbehandlung und Deployment-Infrastruktur
Exit-Codes konsequent als API zwischen Skripten nutzen?
Wir analysieren bestehende Bash-Skripte auf unsichere Exit-Code-Behandlung und implementieren eine dokumentierte Exit-Code-API mit konsistenter Fehlerbehandlung, pipefail und PIPESTATUS-Auswertung für euren Deployment-Stack.
Code-Review
Analyse aller Exit-Code-Verwendungen auf stille Fehler und verdeckte Pipe-Failures
API-Design
Dokumentierte Exit-Code-Bibliothek für konsistente Fehler-Kommunikation zwischen Skripten
Pipeline-Härtung
pipefail, PIPESTATUS und trap ERR nachrüsten – kein Fehler bleibt mehr verborgen
10. Zusammenfassung
Exit-Codes als API zwischen Skripten zu verstehen bedeutet: 0 ist Erfolg, alles andere ist ein spezifischer Zustand, der kommuniziert werden soll. Die Kombination set -euo pipefail stellt sicher, dass kein Nicht-Null-Exit-Code in normalen Befehlen und Pipes verborgen bleibt. PIPESTATUS erlaubt granulare Diagnose innerhalb von Pipe-Ketten. Eigene Exit-Codes als benannte Konstanten definieren macht Skripte zu dokumentierten APIs.
Die Grenzen von set -e zu kennen ist mindestens so wichtig wie es zu nutzen: Bedingungskontexte, ||-Operatoren und Hintergrundprozesse werden nicht automatisch abgefangen. Wer diese Ausnahmen versteht, kann entscheiden, welche Fehlerbehandlung wo explizit nötig ist – und schreibt Skripte, die Fehler weder verschweigen noch übereifrig abbrechen, wo Fehler erwartet und normal sind.
Exit-Codes als API — Das Wichtigste auf einen Blick
Wertebereich
0 = Erfolg. 1 = generischer Fehler. 2 = falsche Verwendung. 126 = nicht ausführbar. 127 = nicht gefunden. 128+N = Signal N.
set -e Grenzen
Löst nicht in if, while, nach || oder && aus. Hintergrundprozesse erfordern explizites wait + Exit-Code-Prüfung.
PIPESTATUS
Sofort nach der Pipe sichern: statuses=("${PIPESTATUS[@]}"). Gibt jeden Schritt einzeln aus – unverzichtbar für Diagnose komplexer Pipes.
Eigene Exit-Code-API
Exit-Codes als readonly-Konstanten definieren, in exit-codes.sh auslagern, dokumentieren. Aufrufer können Fehlertypen unterscheiden und gezielt reagieren.