Jeder Befehl unter Linux gibt am Ende einen Exit-Code zurück — eine kleine Ganzzahl, mit der ein Prozess seinem Aufrufer mitteilt, ob alles glatt lief oder etwas schiefging. In Skripten ist dieser Wert die wichtigste Form der Fehlersignalisierung: Auf ihm bauen if, &&, ||, trap und Optionen wie set -e auf. Wer Exit-Codes nicht versteht, schreibt Skripte, die scheinbar funktionieren — und im Fehlerfall trotzdem 0 zurückgeben.
Konvention
Die Unix-Konvention ist denkbar einfach: 0 bedeutet Erfolg, jeder andere Wert von 1 bis 255 bedeutet einen Fehler. Welcher konkrete Wert für welchen Fehler steht, entscheidet das jeweilige Programm. Diese Asymmetrie hat einen praktischen Grund — es gibt nur einen Weg, etwas richtig zu machen, aber viele Möglichkeiten, daran zu scheitern.
| Wert | Bedeutung |
|---|---|
| 0 | Erfolg, alles in Ordnung |
| 1 bis 255 | Fehler — Bedeutung programmabhängig |
Ein Skript, das ohne explizites exit endet, gibt automatisch den Exit-Code des letzten ausgeführten Befehls zurück. Das ist häufig genau das, was du willst — kann aber zu Überraschungen führen, wenn der letzte Befehl etwa ein echo ist, das immer mit 0 endet, obwohl davor ein Fehler aufgetreten ist.
#!/usr/bin/env bash
ls /nichtvorhanden
echo "fertig"ls: cannot access '/nichtvorhanden': No such file or directory
fertigAufgerufen mit ./skript.sh; echo $? zeigt sich der Wert 0, denn echo "fertig" war der letzte Befehl. Wer den Fehler nach außen tragen will, muss explizit exit setzen oder mit set -e arbeiten.
$? — letzter Exit-Code
Die Spezialvariable $? hält den Exit-Code des zuletzt ausgeführten Vordergrundbefehls. Sie wird nach jedem Befehl neu gesetzt — auch nach einem echo. Wer einen Exit-Code mehrfach prüfen will, sollte ihn deshalb sofort in eine eigene Variable kopieren.
ls /nichtvorhanden
echo "Exit-Code: $?"ls: cannot access '/nichtvorhanden': No such file or directory
Exit-Code: 2Die Falle: Schon das echo selbst hat einen Exit-Code (0). Folgt direkt danach ein zweites echo $?, zeigt es nur noch 0.
ls /nichtvorhanden
rc=$?
echo "rc ist $rc"
echo "rc ist immer noch $rc"Diese Idee — rc=$? als allerersten Befehl nach dem zu prüfenden Aufruf — ist eines der wichtigsten Muster in robusten Skripten und wird auch in Trap-Handlern weiter unten gebraucht.
exit N und return N
Mit exit N beendest du das gesamte Skript und gibst den Wert N an den Aufrufer zurück. Das Pendant für Funktionen ist return N — es verlässt nur die Funktion, nicht das Skript. Beide akzeptieren Werte zwischen 0 und 255.
pruefen() {
[[ -f "$1" ]] || return 1
return 0
}
pruefen "/etc/hostname" || exit 1
echo "Datei existiert"Der Wertebereich ist auf 8 Bit (0-255) begrenzt. Größere Werte werden modulo 256 abgeschnitten — ein klassischer Bug:
bash -c 'exit 256'; echo $?
bash -c 'exit 300'; echo $?
bash -c 'exit -1'; echo $?0
44
255exit 256 wird zu 0 — und damit fälschlich zum Erfolgssignal. exit -1 wird zu 255. Wer Fehler-Codes manuell vergibt, sollte sich auf den Bereich 1 bis 125 beschränken; die Werte ab 126 haben besondere Bedeutung (siehe nächster Abschnitt).
Standard-Fehlercodes
Über die Jahre haben sich einige Konventionen für bestimmte Exit-Codes etabliert. Sie sind nicht in einem einzelnen Standard festgeschrieben — die Bash-Manpage, BSDs sysexits.h und einzelne Tools beschreiben jeweils Teile davon — aber so weit verbreitet, dass man sie kennen sollte.
| Code | Bedeutung | Quelle |
|---|---|---|
| 0 | Erfolg | Konvention |
| 1 | Allgemeiner Fehler | Konvention |
| 2 | Falsche Verwendung von Shell-Builtins, Syntaxfehler | Bash |
| 126 | Datei gefunden, aber nicht ausführbar | Shell |
| 127 | Befehl nicht gefunden | Shell |
| 128 | Ungültiges Argument für exit | Bash |
| 128 + N | Durch Signal N beendet (z. B. 130 = SIGINT, Strg+C; 143 = SIGTERM) | Shell |
| 255 | Exit-Status außerhalb des Bereichs | Shell |
Programme definieren zusätzlich eigene Codes. grep etwa unterscheidet bewusst zwischen „kein Treffer” und „Fehler”:
| Tool | Code | Bedeutung |
|---|---|---|
| grep | 0 | Mindestens ein Treffer |
| grep | 1 | Kein Treffer (kein Fehler!) |
| grep | 2 | Echter Fehler (z. B. Datei nicht lesbar) |
| diff | 0 | Dateien identisch |
| diff | 1 | Dateien unterscheiden sich |
| diff | 2 | Fehler |
| test / [[ ]] | 0 | Bedingung wahr |
| test / [[ ]] | 1 | Bedingung falsch |
Wichtig: Bei grep und diff ist 1 normales Verhalten, kein Fehler. Wer das nicht weiß, schreibt Skripte, die mit set -e reihenweise abbrechen, wenn grep einfach nichts findet.
Pipes und Exit-Codes
Eine Pipeline cmd1 | cmd2 | cmd3 startet alle drei Befehle parallel. Welcher Exit-Code zählt am Ende? Standardmäßig nur der des letzten Befehls — also der von cmd3. Die Codes der vorderen Stufen gehen verloren, was in Skripten oft genau das Gegenteil dessen ist, was man will.
false | true
echo $?0false schlägt mit 1 fehl, true liefert 0 — und die Pipeline gilt als erfolgreich. Mit set -o pipefail wechselt Bash das Verhalten: Die Pipeline liefert dann den Exit-Code des am weitesten links gelegenen Befehls, der mit Fehler endete; nur wenn alle Stufen 0 zurückgeben, ist auch die Pipeline 0.
set -o pipefail
false | true
echo $?1Empfehlung: In jedem ernsthaften Skript gehört set -o pipefail an den Anfang — meistens zusammen mit set -e und set -u als set -euo pipefail.
set -e (errexit)
set -e (kurz für errexit) lässt das Skript beim ersten fehlschlagenden Befehl abbrechen — in der Theorie. In der Praxis hat die Option eine Reihe von Ausnahmen, die regelmäßig zu Verwirrung führen. Ein Befehl mit Fehler-Exit beendet das Skript nicht, wenn er an einer der folgenden Stellen steht:
- in der Bedingung von
if,while,until - vor
&&oder||(außer es ist der letzte Befehl) - mit vorangestelltem
!(Negation) - als nicht-letzter Befehl in einer Pipe (sofern
pipefailnicht aktiv ist)
set -e
if grep "muster" datei.txt; then
echo "gefunden"
fi
echo "weiter"Findet grep nichts, liefert es Exit-Code 1. Trotz set -e läuft das Skript weiter, weil grep Teil der if-Bedingung ist — genau das soll if ja prüfen. Das ist beabsichtigt und in den meisten Fällen die richtige Semantik.
Die häufigste Falle ist die Kombination mit Pipes: cmd | grep "x" bricht nicht ab, wenn cmd fehlschlägt, weil nur der Exit-Code von grep gewertet wird. Erst set -o pipefail macht set -e an dieser Stelle zuverlässig.
set -euo pipefail
cat /nichtvorhanden | grep "x"
echo "wird nicht erreicht"PIPESTATUS-Array
Manchmal ist pipefail zu grob — du willst wissen, welche Stufe der Pipeline genau fehlschlug, oder unterschiedlich auf verschiedene Fehler reagieren. Bash stellt dafür das Array PIPESTATUS bereit: Pro Stufe ein Eintrag, in der Reihenfolge der Pipe.
false | true | false
echo "${PIPESTATUS[@]}"
echo "Erste Stufe: ${PIPESTATUS[0]}"
echo "Zweite Stufe: ${PIPESTATUS[1]}"
echo "Dritte Stufe: ${PIPESTATUS[2]}"1 0 1
Erste Stufe: 1
Zweite Stufe: 0
Dritte Stufe: 1PIPESTATUS ist nur direkt nach der Pipeline gültig — schon der nächste Befehl überschreibt das Array. Wer es weiterverwenden will, kopiert es in eine eigene Variable: codes=("${PIPESTATUS[@]}").
Praxis-Patterns
Die folgenden Muster decken die häufigsten Anwendungsfälle in Skripten ab. Jedes lässt sich isoliert verwenden, viele tauchen in Kombination am Anfang produktiver Skripte auf.
Status-Check kompakt
cmd && echo OK || echo FAILDie Kette && ... || ... ist die kürzeste Form einer Erfolgsverzweigung. Vorsicht: Wenn der Befehl im &&-Zweig (hier echo OK) selbst fehlschlägt, läuft der ||-Zweig zusätzlich — für robuste Logik ist if/else die sicherere Wahl.
Skript-Header
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'set -e bricht bei Fehlern ab, set -u bei undefinierten Variablen, set -o pipefail propagiert Pipe-Fehler korrekt. Die IFS-Zuweisung verhindert das berüchtigte Wort-Splitting an Leerzeichen — eine Quelle vieler Skript-Bugs. Diese drei Zeilen sind die Mindestausstattung für jedes Skript, das im Fehlerfall nichts kaputtmachen darf.
Pipeline mit voller Status-Kontrolle
cmd1 | cmd2 | cmd3
echo "Codes: ${PIPESTATUS[@]}"
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
echo "cmd1 ist fehlgeschlagen"
fiSo lässt sich gezielt auf eine bestimmte Stufe reagieren — etwa eine fehlende Eingabedatei in cmd1 anders behandeln als einen grep-Misserfolg in cmd2.
Cleanup-Trap mit Exit-Code-Erhalt
cleanup() {
rm -f /tmp/skript.lock
}
trap 'rc=$?; cleanup; exit $rc' EXITDer Trap auf EXIT läuft bei jedem Skript-Ende — egal ob normal, durch exit, durch Signal oder durch set -e. rc=$? sichert den Exit-Code als allerersten Schritt; ohne diese Zeile würden Befehle innerhalb von cleanup den Wert überschreiben und das Skript würde am Ende mit dem Status der Aufräumarbeit terminieren.
Conditional Bail mit Fehlermeldung
cmd || { echo "fail: cmd lieferte $?" >&2; exit 1; }Schlägt cmd fehl, schreibt der Block eine Meldung auf stderr und beendet das Skript mit Code 1. Die geschweiften Klammern fassen mehrere Befehle in derselben Shell zusammen — runde Klammern würden eine Subshell aufmachen, dann hätte exit keine Wirkung auf das Hauptskript.
Häufige Stolperfallen
set -e ist trügerisch — viele Befehle brechen nicht ab
Die Option greift bewusst nicht in if/while/until-Bedingungen, nicht vor &&/||, nicht bei mit ! negierten Befehlen und nicht bei nicht-letzten Pipe-Stufen ohne pipefail. Wer set -e setzt und sich darauf verlässt, dass jeder Fehler abbricht, übersieht regelmäßig genau diese Ausnahmen. Praxis: set -e immer mit pipefail kombinieren und kritische Stellen zusätzlich explizit prüfen — entweder mit cmd || exit 1 oder mit einem dedizierten if-Block, der den Fehler aktiv behandelt.
set -e plus cmd | grep braucht pipefail
Ohne pipefail zählt nur der letzte Pipe-Befehl. Schreibst du kritischer-job | grep WARN, läuft das Skript weiter, selbst wenn kritischer-job mit Fehler endet — denn grep hat ja sauber 0 oder 1 zurückgegeben. Das ist eine der häufigsten Ursachen für Skripte, die scheinbar funktionieren und im Hintergrund stillschweigend Fehler schlucken. Lösung: immer set -euo pipefail verwenden, oder gezielt ${PIPESTATUS[0]} prüfen.
exit-Werte über 255 wrappen modulo 256
Ein typischer Bug entsteht, wenn der Code direkt aus einem Tool weitergereicht wird, das einen größeren Wert oder etwa einen libc-Errno-Code liefert. exit 256 wird zu 0 — also ausgerechnet zum Erfolgssignal — und exit 300 zu 44. Auch negative Werte wie exit -1 funktionieren nicht so wie aus C gewohnt; in Bash ergibt das 255. Halte dich beim manuellen Vergeben an den Bereich 1 bis 125 und reserviere die hohen Werte für die Shell.
Subshell-Exit-Codes propagieren nicht direkt
Ein Block in runden Klammern öffnet eine Subshell: (cmd; exit 5) setzt $? im Eltern-Skript zwar auf 5, ein darin gesetztes set -e oder eine Variablenänderung wirkt aber nur innerhalb der Subshell. Wer Code-Blöcke gruppieren will, ohne eine Subshell zu erzeugen, nutzt geschweifte Klammern: { cmd; exit 5; }. Wichtig sind dort die Leerzeichen und das abschließende Semikolon.
0/1 bei diff und grep ist nicht zwingend ein Fehler
Bei diff bedeutet 1, dass sich die Dateien unterscheiden — eine Information, kein Fehler. Bei grep bedeutet 1, dass kein Treffer gefunden wurde. Erst Code 2 ist ein echter Fehler. Mit set -e brechen Skripte hier reihenweise ab, obwohl alles in Ordnung ist. Korrekturen: diff a b || true oder eine explizite if-Prüfung, die die drei Fälle (gleich, verschieden, Fehler) unterscheidet.
Trap-Handler überschreiben $?, wenn nicht früh gesichert
Innerhalb eines Trap-Handlers ist $? zunächst der Code, mit dem das Skript ankam — aber jeder Befehl im Handler überschreibt ihn. Wer exit ohne Argument am Ende des Handlers schreibt, exitet mit dem Status des letzten Cleanup-Befehls, nicht mit dem ursprünglichen Fehler. Daher: rc=$? als allererste Zeile im Handler, dann aufräumen, am Ende exit "$rc". Das gilt insbesondere für EXIT- und ERR-Traps, wo der Exit-Code die einzige Information darüber ist, wie das Skript geendet hat.
set -e und exit im Trap zusammen — letzter Befehl entscheidet
Mit set -e bricht das Skript schon beim ersten Fehler ab und springt in den Trap. Wer dort weitere Befehle ausführt, die selbst fehlschlagen können, riskiert, dass der ursprüngliche Fehler verloren geht — der Handler endet dann mit dem Exit-Code seines eigenen letzten Befehls. Auch hier ist die Lösung, den ursprünglichen Code früh zu sichern und am Ende explizit exit "$rc" aufzurufen, statt sich auf das implizite Exit-Verhalten zu verlassen.
Weiterführende Ressourcen
Externe Quellen
- Bash-Manpage: EXIT STATUS (man7.org) — Offizielle Definition aller Bash-Exit-Codes
- GNU Bash-Handbuch: Exit Status — Vollständige Referenz mit Beispielen
- GNU Bash-Handbuch: The Set Builtin —
set -e,set -o pipefailund alle weiteren Optionen - sysexits.h (FreeBSD) — BSD-Konventionen für strukturierte Exit-Codes (64-78)
- Greg’s Wiki: BashFAQ/105 — Why doesn’t set -e do what I expected? — Detaillierte Analyse der
set -e-Fallen
Verwandte Artikel
- Linux Streams und Pipes — stdin, stdout, stderr und Pipe-Mechanik im Detail
- Linux Shell — Bash-Grundlagen und Built-ins
- Shell-Scripting Bedingungen —
if,test,[[ ]]und Vergleichsoperatoren - Shell-Scripting Funktionen — Funktionen,
returnund lokale Variablen - Linux Manpages — Hilfeseiten effizient durchsuchen