====== 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