====== Lyx – Exit-Codes ======
Der Exit-Code ist die einzige Information, die ein beendetes Programm seinem Aufrufer **maschinenlesbar** hinterlässt. Alles andere — Meldungen, Logs, Reports — muss gelesen werden; der Exit-Code lässt sich abfragen. Plattformübergreifend gilt eine einzige feste Regel:
> **0 heißt Erfolg. Alles andere heißt Fehler, Warnung oder anwendungsspezifischer Status.**
Eine allgemeingültige Tabelle für Anwendungen gibt es **nicht** — nur Konventionen. Diese Seite beschreibt, wie Lyx den Exit-Code setzt, welche Konventionen gelten und wie man eigene Codes entwirft, ohne in die üblichen Fallen zu laufen.
→ [[lyx_-_programmiersprache:sprache:exception-handling|Fehlerbehandlung]] · [[lyx_-_programmiersprache:units:process|std.process]] · [[lyx_-_programmiersprache:units:io|std.io]] · [[lyx_-_programmiersprache:tools:compiler-parameter|Compiler-Parameter]]
----
===== 1. Der Rückgabewert von main =====
Jedes Lyx-Programm beginnt bei ''main''. Deklariert man sie mit Rückgabetyp ''int64'', **ist ihr Rückgabewert der Exit-Code des Prozesses**:
fn main(): int64 {
return 0; // Erfolg
}
$ ./programm
$ echo $?
0
Der Weg dahin: Der vom Compiler erzeugte ''_start''-Stub ruft ''main'' auf, nimmt deren Rückgabewert und übergibt ihn unverändert an den ''exit_group''-Syscall. Es gibt keine Laufzeitschicht dazwischen, die den Wert noch verändert — was ''main'' zurückgibt, geht direkt an den Kernel.
==== Die drei Wege aus einem Programm ====
^ Weg ^ Exit-Code ^
| ''return N'' aus ''main'' | ''N'' (auf 8 Bit gestutzt, siehe unten) |
| ''exit(N)'' — Builtin, überall aufrufbar, kein Import nötig | ''N'' — beendet sofort, auch aus einer tief verschachtelten Funktion |
| ''panic(...)'', fehlgeschlagenes ''assert'', Bereichsverletzung, ungefangenes ''throw'' | **1** |
import std.io;
fn PruefeKonfiguration(pfad: pchar): void {
if (!FileExists(pfad)) {
PrintLn("Konfiguration nicht lesbar");
exit(3); // beendet das Programm hier, kein return noetig
}
}
''std.io'' bietet dafür zusätzlich ''ExitProc(code)'' — ein reiner Namensalias für ''exit''.
==== main ohne Rückgabewert ====
''%%fn main(): void%%'' ist zulässig und ergibt immer Exit-Code **0**. Wer einen anderen Code braucht, nimmt ''int64'' oder ''exit()''.
''%%fn main(): int64%%'' **ohne** ''return'' im Rumpf wird dagegen abgewiesen — seit der Typprüfung von 1.0.14 meldet der Compiler ''Funktion ohne return, Rueckgabetyp verlangt einen Wert 'main'''.
----
===== 2. Die Modulo-256-Falle =====
Unter Linux und macOS überträgt der Kernel nur die **unteren 8 Bit** des Werts. Größere Zahlen laufen still über:
fn main(): int64 { return 256; } // -> Exit-Code 0 (!)
Nachgemessen mit lyxc 1.0.16F:
^ ''return'' ^ tatsächlicher Exit-Code ^
| ''0'' | 0 |
| ''1'' | 1 |
| ''42'' | 42 |
| ''255'' | 255 |
| ''256'' | **0** |
| ''257'' | 1 |
| ''300'' | 44 |
| ''1000'' | 232 |
| ''-1'' | 255 |
| ''-2'' | 254 |
> **Nie eine Anzahl als Exit-Code zurückgeben.** ''%%return fehlerAnzahl;%%'' sieht sinnvoll aus und funktioniert 255 Läufe lang — beim 256. gemeldeten Fehler meldet das Programm **Erfolg**. Dasselbe gilt für Summen, Zeilennummern und alles andere, was wachsen kann.
>
> Der einzige verlässliche Weg: eine kleine, feste Menge von Codes, jede mit einer Bedeutung. Die Anzahl gehört in die Ausgabe, nicht in den Exit-Code.
Auf **Windows** ist der Exit-Code ein 32-Bit-Wert und wird nicht gestutzt. Wer beide Zielplattformen bedient, hält sich trotzdem an den Unix-Bereich — sonst verhält sich dasselbe Programm je nach Ziel anders.
----
===== 3. Konventionen, an die man sich halten sollte =====
==== Unix / POSIX ====
^ Code ^ Bedeutung ^
| ''0'' | Erfolg |
| ''1''–''125'' | Allgemeiner oder anwendungsspezifischer Fehler — **hier gehören eigene Codes hin** |
| ''126'' | Kommando gefunden, aber nicht ausführbar (Shell-Konvention) |
| ''127'' | Kommando nicht gefunden (Shell-Konvention) |
| ''128 + n'' | Durch Signal ''n'' beendet: 130 = SIGINT (Strg+C), 137 = SIGKILL, 143 = SIGTERM |
Die Codes ab 126 sind belegt: Ein eigenes ''exit(137)'' ist von einem OOM-Kill nicht mehr zu unterscheiden. Der Bereich **1–125** ist der einzige, der einem allein gehört.
==== sysexits.h — die BSD-Empfehlung ====
Wo eine feinere Einteilung gebraucht wird, ist ''sysexits.h'' die verbreitetste Konvention (verpflichtend ist sie nirgends):
^ Konstante ^ Wert ^ Bedeutung ^
| ''EX_OK'' | 0 | Erfolg |
| ''EX_USAGE'' | 64 | Falsche Kommandozeilenverwendung |
| ''EX_DATAERR'' | 65 | Ungültige Eingabedaten |
| ''EX_NOINPUT'' | 66 | Eingabedatei fehlt oder ist unlesbar |
| ''EX_UNAVAILABLE'' | 69 | Dienst nicht verfügbar |
| ''EX_SOFTWARE'' | 70 | Interner Softwarefehler |
| ''EX_OSERR'' | 71 | Betriebssystemfehler |
| ''EX_CANTCREAT'' | 73 | Ausgabedatei nicht erstellbar |
| ''EX_IOERR'' | 74 | I/O-Fehler |
| ''EX_TEMPFAIL'' | 75 | **Temporärer** Fehler — ein Wiederholungsversuch ist sinnvoll |
| ''EX_NOPERM'' | 77 | Fehlende Berechtigung |
| ''EX_CONFIG'' | 78 | Konfigurationsfehler |
''EX_TEMPFAIL'' ist der nützlichste Eintrag der Liste: Er beantwortet die Frage, die ein automatischer Aufrufer wirklich hat — //noch einmal versuchen oder aufgeben?// Mailserver werten ihn bis heute so aus.
==== Windows ====
* Der Exit-Code ist ein 32-Bit-DWORD, gelesen mit ''GetExitCodeProcess''.
* **259 ist tabu**: Der Wert bedeutet ''STILL_ACTIVE''. Ein Prozess, der mit 259 endet, gilt für jede Überwachung als noch laufend.
* ''3010'' heißt bei Installern „Erfolg, Neustart erforderlich"; ''9009'' meldet ''cmd.exe'' für „Befehl nicht gefunden".
* Große negative Codes in CI-Protokollen sind **Abstürze**, kein ''return'': ''-1073741819'' ist ''0xC0000005'', eine Access Violation.
----
===== 4. Eigene Exit-Codes entwerfen =====
- **Exit-Codes sind eine API.** Sobald sie dokumentiert und in fremden Skripten verbaut sind, lassen sie sich nicht mehr ändern. Deshalb klein halten und früh festlegen.
- **Nur 1–125 verwenden.** 126 und darüber ist unter Unix belegt, 259 unter Windows.
- **Grobe Klassen statt Fehler-Enzyklopädie.** Der Exit-Code beantwortet: //Was soll der Aufrufer tun?// (Wiederholen? Konfiguration reparieren? Bug melden?) Das //Warum// gehört nach stderr.
- **Der interne Fehler bekommt einen eigenen Code.** „Melde einen Bug" ist eine andere Handlungsaufforderung als „deine Eingabe ist kaputt". ''rustc'' macht das mit 101 vor.
- **Temporär von dauerhaft trennen**, wenn Wiederholungen denkbar sind — sonst versucht es der Aufrufer entweder nie oder immer.
- **Keine Bitmasken.** ''robocopy'' liefert 1 = kopiert, 2 = Extras, 4 = Unterschiede, und erst ab 8 ist es ein Fehler. Damit ist es mit der gesamten „≠ 0 = Fehler"-Welt inkompatibel und muss überall gesondert behandelt werden.
- **Nie eine Anzahl** (siehe Abschnitt 2).
import std.io;
con EXIT_OK: int64 := 0;
con EXIT_EINGABE: int64 := 1; // Datei fehlt oder ist unlesbar
con EXIT_FORMAT: int64 := 2; // Datei da, Inhalt unbrauchbar
con EXIT_TEMPORAER: int64 := 3; // Netz/Dienst weg — Wiederholung sinnvoll
con EXIT_INTERN: int64 := 70; // Bug in diesem Programm
fn main(): int64 {
var daten: int64 := LadeEingabe();
if (daten == 0) {
PrintLn("Eingabe nicht lesbar"); // das Warum nach stderr/stdout
return EXIT_EINGABE; // das Was-tun als Exit-Code
}
return EXIT_OK;
}
----
===== 5. Exit-Codes von Kindprozessen lesen =====
''[[lyx_-_programmiersprache:units:process|std.process]]'' liefert den Exit-Code eines Kindprozesses bereits **ausgepackt** — die Unit wendet ''WEXITSTATUS'' an, man bekommt also direkt die 0–255 und nicht den rohen ''wait''-Status:
import std.io;
import std.process;
fn main(): int64 {
var code: int64 := shell("exit 42"c);
PrintLn(IntToStr(code)); // 42
return 0;
}
> **Ein per Signal beendetes Kind liefert ''0'' — nicht 137 oder 143.** Die Unit liest ''(status >> 8) & 255''; bei einem Signaltod steht dort nichts, das Ergebnis ist ''0'' und damit von einem Erfolg **nicht zu unterscheiden**. Nachgemessen: ''%%shell("kill -KILL $$")%%'' ergibt ''0''.
>
> Das ''128 + n''-Schema kennt nur die **Shell**, die es aus der Signalinformation zusammensetzt. Wer erkennen muss, ob ein Kind abgeschossen wurde, führt den Aufruf über eine Shell (''%%shell("...; echo $?")%%'') oder wertet den rohen ''wait''-Status selbst aus.
>
> Zweite Folge derselben Mechanik: ''%%shell("exit 256")%%'' ergibt ''0'' — die Modulo-256-Falle wirkt auch hier.
Schlägt ''execve'' in einem mit ''spawn'' gestarteten Kind fehl, beendet sich das Kind mit **127** — dieselbe Konvention wie die Shell für „Kommando nicht gefunden".
----
===== 6. Die Exit-Codes von lyxc =====
Der Compiler selbst benutzt (Stand 1.0.16F, nachgemessen):
^ Code ^ Bedeutung ^ Was der Aufrufer tun sollte ^
| ''0'' | Erfolg — auch mit Warnungen | weitermachen |
| ''1'' | Übersetzung fehlgeschlagen: Parse-, Sema- oder Typfehler; ebenso fehlende Eingabedatei und Aufruf ohne Argumente | Quellcode bzw. Pfad prüfen |
| ''2'' | Ungültige Kommandozeile: unbekannter Schalter, ''%%--%%target-energy'' außerhalb 1..5, ''-O'' ohne Stufe | Aufruf korrigieren |
Syntax- und Semantikfehler werden **bewusst nicht** über verschiedene Codes getrennt: Kein Aufrufer handelt darauf unterschiedlich, der Unterschied gehört in die Fehlermeldung. Warnungen ergeben 0.
> **Die Spezifikation sieht mehr Codes vor, als 1.0.16F umsetzt.** Vorgesehen sind zusätzlich ''3'' (Eingabe fehlt/unlesbar), ''4'' (Konfigurationsfehler), ''5'' (Backend-/Umgebungsfehler, etwa fehlender Linker) und ''70'' (interner Compilerfehler, ICE — „Bug melden", angelehnt an ''EX_SOFTWARE'').
>
> Gemessen liefert 1.0.16F für eine fehlende Eingabedatei ''1'' statt ''3''. Wer CI-Skripte schreibt, prüft deshalb heute nur auf ''0'' / ''1'' / ''2'' und nicht auf feinere Codes.
Für ein Build-Skript heißt das:
lyxc main.lyx -o main
case $? in
0) echo "gebaut" ;;
1) echo "Quellcode fehlerhaft"; exit 1 ;;
2) echo "Aufruf falsch — lyxc --help"; exit 2 ;;
esac
----
===== 7. Fallen in Shell und CI =====
* ''$?'' gilt nur für das **zuletzt** ausgeführte Kommando. Wer ihn zweimal braucht, sichert ihn sofort in einer Variablen.
* Der Status einer **Pipeline** ist der des letzten Glieds: ''%%lyxc x.lyx | tee log%%'' meldet Erfolg, auch wenn ''lyxc'' scheitert. Gegenmittel: ''set -o pipefail'' oder ''${PIPESTATUS[@]}''.
* ''set -e'' hat so viele Ausnahmen (Bedingungen, ''&&''/''||''-Ketten, Subshells), dass es kein Sicherheitsnetz ist.
* ''ssh'' benutzt **255** für eigene Verbindungsfehler und reicht sonst den entfernten Status durch — ein Programm, das selbst mit 255 endet, ist über ''ssh'' nicht mehr von einem Verbindungsabbruch zu unterscheiden.
* In **Docker und Kubernetes** heißt ''137'' fast immer OOM-Kill und ''143'' regulärer Shutdown per SIGTERM. Beides sind keine Anwendungsfehler — vor dem Debuggen den ''OOMKilled''-Status prüfen.
* In ''cmd.exe'' bedeutet ''IF ERRORLEVEL 5'' **größer oder gleich 5**. Genau prüfen mit ''IF %ERRORLEVEL% EQU 5''.
* In PowerShell ist ''$?'' ein Wahrheitswert des letzten PowerShell-Ausdrucks; der Exit-Code eines nativen Programms steht in ''$LASTEXITCODE''.
----
Geprüft mit lyxc 1.0.16F.
Letzte Aktualisierung: 2026-08-11