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.

WertBedeutung
0Erfolg, alles in Ordnung
1 bis 255Fehler — 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.

Bash Letzter Befehl bestimmt Skript-Exit
#!/usr/bin/env bash
ls /nichtvorhanden
echo "fertig"
Output
ls: cannot access '/nichtvorhanden': No such file or directory
fertig

Aufgerufen 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.

Bash Exit-Code lesen
ls /nichtvorhanden
echo "Exit-Code: $?"
Output
ls: cannot access '/nichtvorhanden': No such file or directory
Exit-Code: 2

Die Falle: Schon das echo selbst hat einen Exit-Code (0). Folgt direkt danach ein zweites echo $?, zeigt es nur noch 0.

Bash $? sicher in Variable kopieren
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.

Bash exit beendet Skript, return die Funktion
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 Wert > 255 wrappt
bash -c 'exit 256'; echo $?
bash -c 'exit 300'; echo $?
bash -c 'exit -1'; echo $?
Output
0
44
255

exit 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.

CodeBedeutungQuelle
0ErfolgKonvention
1Allgemeiner FehlerKonvention
2Falsche Verwendung von Shell-Builtins, SyntaxfehlerBash
126Datei gefunden, aber nicht ausführbarShell
127Befehl nicht gefundenShell
128Ungültiges Argument für exitBash
128 + NDurch Signal N beendet (z. B. 130 = SIGINT, Strg+C; 143 = SIGTERM)Shell
255Exit-Status außerhalb des BereichsShell

Programme definieren zusätzlich eigene Codes. grep etwa unterscheidet bewusst zwischen „kein Treffer” und „Fehler”:

ToolCodeBedeutung
grep0Mindestens ein Treffer
grep1Kein Treffer (kein Fehler!)
grep2Echter Fehler (z. B. Datei nicht lesbar)
diff0Dateien identisch
diff1Dateien unterscheiden sich
diff2Fehler
test / [[ ]]0Bedingung wahr
test / [[ ]]1Bedingung 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.

Bash Default: nur letzter Code zählt
false | true
echo $?
Output
0

false 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.

Bash Mit pipefail erkennt Bash den Fehler
set -o pipefail
false | true
echo $?
Output
1

Empfehlung: 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 pipefail nicht aktiv ist)
Bash set -e greift nicht in if-Bedingungen
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.

Bash errexit + pipefail gehören zusammen
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.

Bash PIPESTATUS pro Stufe
false | true | false
echo "${PIPESTATUS[@]}"
echo "Erste Stufe: ${PIPESTATUS[0]}"
echo "Zweite Stufe: ${PIPESTATUS[1]}"
echo "Dritte Stufe: ${PIPESTATUS[2]}"
Output
1 0 1
Erste Stufe: 1
Zweite Stufe: 0
Dritte Stufe: 1

PIPESTATUS 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

Bash Erfolg oder Fehler melden
cmd && echo OK || echo FAIL

Die 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

Bash Robuster Skript-Anfang
#!/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

Bash Alle Codes der Pipe einsehen
cmd1 | cmd2 | cmd3
echo "Codes: ${PIPESTATUS[@]}"
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    echo "cmd1 ist fehlgeschlagen"
fi

So 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

Bash Aufräumen ohne den Code zu verlieren
cleanup() {
    rm -f /tmp/skript.lock
}

trap 'rc=$?; cleanup; exit $rc' EXIT

Der 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

Bash Aussagekräftig abbrechen
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

/ Weiter

Zurück zu Shell-Scripting

Zur Übersicht