Umgebungsvariablen und Prozesse in Rust
Jedes Programm läuft in einer Umgebung (Environment). Das Betriebssystem gibt ihm eine Reihe benannter Textwerte mit, etwa PATH, HOME oder PORT. Das sind Umgebungsvariablen. Mit ihnen konfigurierst du ein Programm, ohne seinen Code zu ändern: Dieselbe Binärdatei kann auf deinem Laptop Port 8080 nutzen und auf einem Server Port 80.
Ein Programm lebt außerdem zwischen anderen Programmen. Es kann mit einem Exit-Code enden, der dem Aufrufer sagt: „hat geklappt“ oder „ist schiefgegangen“. Und es kann selbst andere Programme starten. In Rust findest du die Werkzeuge dafür in zwei Modulen der Standardbibliothek: std::env und std::process. Du brauchst kein zusätzliches Crate.
Eine Variable mit env::var lesen
Abschnitt betitelt „Eine Variable mit env::var lesen“std::env::var bekommt einen Namen und gibt ein Result zurück. Es ist Ok(String), wenn die Variable existiert, und Err, wenn sie fehlt (oder kein gültiges Unicode enthält). Eine fehlende Variable ist ein ganz normaler Fall, deshalb zwingt Rust dich, ihn zu behandeln:
use std::env;
fn main() { // Ein Name, der auf fast keinem Rechner gesetzt ist. match env::var("RUSTTUTORIAL_API_KEY") { Ok(value) => println!("Gefunden: {value}"), Err(e) => println!("Nicht verfügbar: {e}"), }
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080")); println!("Port: {port}");}Ausgabe:
Nicht verfügbar: environment variable not foundPort: 8080Die Meldung environment variable not found kommt von Rust selbst und bleibt deshalb Englisch. Sie bedeutet „Umgebungsvariable nicht gefunden“.
Nimm match, wenn du auf den fehlenden Fall reagieren willst. Nimm .unwrap_or(default), wenn eine fehlende Variable einfach „nimm den Standardwert“ bedeutet.
Standardwerte, Zahlen und Flags
Abschnitt betitelt „Standardwerte, Zahlen und Flags“Umgebungsvariablen sind immer Text. Brauchst du eine Zahl, musst du sie parsen – und auch das kann schiefgehen. Ein typisches Muster: Mach aus dem Result mit .ok() eine Option, versuche zu parsen und greif auf einen Standardwert zurück, falls etwas nicht klappt:
use std::env;
fn main() { let workers: u32 = env::var("RUSTTUTORIAL_WORKERS") .ok() .and_then(|text| text.parse().ok()) .unwrap_or(4);
let debug = env::var("RUSTTUTORIAL_DEBUG").is_ok();
println!("workers = {workers}"); println!("debug = {debug}");}Ausgabe:
workers = 4debug = falseMit .is_ok() behandelst du eine Variable schnell als Ein/Aus-Schalter: Ist sie überhaupt gesetzt, ist das Flag an.
var_os, vars und Variablen setzen
Abschnitt betitelt „var_os, vars und Variablen setzen“env::var_os gibt Option<OsString> statt eines Result zurück. Es scheitert nie an ungewöhnlichen Bytes und eignet sich deshalb gut für Pfade. env::vars() liefert dir alle Variablen als Paare (name, value).
env::set_var ändert eine Variable für den aktuellen Prozess. Achte auf den unsafe-Block drumherum:
use std::env;
fn main() { unsafe { env::set_var("MYAPP_COLOR", "blue"); env::set_var("MYAPP_SIZE", "large"); }
println!("{:?}", env::var_os("MYAPP_COLOR")); println!("{:?}", env::var_os("MYAPP_MISSING"));
let mut mine: Vec<(String, String)> = env::vars() .filter(|(key, _)| key.starts_with("MYAPP_")) .collect(); mine.sort(); // die Reihenfolge von env::vars() ist nicht garantiert
for (key, value) in &mine { println!("{key}={value}"); }}Ausgabe:
Some("blue")NoneMYAPP_COLOR=blueMYAPP_SIZE=largeWarum ist set_var unsafe? Auf vielen Betriebssystemen ist die Umgebung eine einzige, gemeinsame Tabelle für den ganzen Prozess. Ändert ein Thread sie, während ein anderer Thread (oder eine C-Bibliothek) sie liest, kann das Programm abstürzen. Rust kann das nicht für dich prüfen. Deshalb sind set_var und remove_var seit Edition 2024 unsafe-Funktionen. In Edition 2021 kompilieren sie noch ohne unsafe, aber mit dem Block funktioniert der Code in beiden. Die sichere Regel: Setze Variablen nur ganz am Anfang von main, bevor du Threads startest. Noch besser: Ändere deine eigene Umgebung gar nicht. Willst du einem Kindprogramm eine Variable mitgeben, nimm Command::env (siehe unten). Was unsafe genau bedeutet, erklärt die Seite Unsafe Rust.
Das aktuelle Verzeichnis
Abschnitt betitelt „Das aktuelle Verzeichnis“env::current_dir() gibt den Ordner zurück, aus dem das Programm gestartet wurde, als PathBuf. Relative Pfade wie "config.toml" werden von dort aus aufgelöst. Der echte Pfad hängt von deinem Rechner ab. Darum gibt dieses Beispiel nur Dinge aus, die überall stimmen:
use std::env;
fn main() { let dir = env::current_dir().expect("konnte das aktuelle Verzeichnis nicht lesen"); println!("Ist der Pfad absolut? {}", dir.is_absolute());
let config = dir.join("config.toml"); println!("Endet mit config.toml? {}", config.ends_with("config.toml"));}Ausgabe:
Ist der Pfad absolut? trueEndet mit config.toml? trueGib auf deinem eigenen Rechner dir.display() aus, um den vollständigen Pfad zu sehen. Auch die Kommandozeilenargumente stecken in std::env; sie haben eine eigene Seite: Kommandozeilenargumente.
Exit-Codes mit process::exit
Abschnitt betitelt „Exit-Codes mit process::exit“Wenn ein Programm endet, gibt es demjenigen, der es gestartet hat, eine Zahl zurück. 0 heißt Erfolg, alles andere heißt Fehler. Shell-Skripte und CI-Systeme prüfen diese Zahl. std::process::exit(code) beendet das Programm sofort mit diesem Code:
use std::env;use std::process;
fn main() { println!("Starte...");
let token = match env::var("RUSTTUTORIAL_TOKEN") { Ok(t) => t, Err(_) => { eprintln!("Fehler: RUSTTUTORIAL_TOKEN ist nicht gesetzt"); process::exit(1); } };
println!("Verwende Token {token}");}Ausgabe:
Starte...Die Fehlerzeile wird mit eprintln! ausgegeben. Sie landet also auf stderr, nicht auf stdout. Der Playground zeigt sie in einem eigenen Bereich, zusammen mit dem Exit-Status.
Zwei Dinge solltest du wissen. Erstens: process::exit beendet das Programm sofort. Destruktoren (Drop) für Werte, die noch leben, laufen also nicht. Zweitens: Oft brauchst du es gar nicht. Wenn main ein Result<(), E> zurückgibt und du Err zurückgibst, gibt Rust den Fehler aus und beendet das Programm für dich mit Code 1.
Andere Programme mit Command starten
Abschnitt betitelt „Andere Programme mit Command starten“std::process::Command startet ein anderes Programm. Du baust den Aufruf Schritt für Schritt: zuerst den Programmnamen, dann jedes Argument mit .arg() oder .args(). Danach entscheidest du, wie es laufen soll:
.output()wartet, bis es fertig ist, und fängt stdout und stderr ein..status()wartet ebenfalls, lässt die Ausgabe aber direkt in dein Terminal laufen.
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("Hallo") .arg("aus einem Kindprozess") .output() .expect("konnte echo nicht starten");
let text = String::from_utf8_lossy(&output.stdout); println!("Erfolg: {}", output.status.success()); println!("stdout: {}", text.trim());}Ausgabe:
Erfolg: truestdout: Hallo aus einem Kindprozessoutput.stdout sind rohe Bytes (Vec<u8>), weil ein Programm alles Mögliche ausgeben kann. String::from_utf8_lossy macht daraus Text.
Hier liest .status() einen Exit-Code, und .env() gibt nur dem Kindprozess eine Variable mit:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "exit 3"]) .status() .expect("konnte sh nicht starten");
println!("Erfolg: {}", status.success()); println!("Code: {:?}", status.code());
let output = Command::new("sh") .args(["-c", "echo \"$GREETING, $NAME\""]) .env("GREETING", "Hallo") .env("NAME", "Ferris") .output() .expect("konnte sh nicht starten"); print!("{}", String::from_utf8_lossy(&output.stdout));}Ausgabe:
Erfolg: falseCode: Some(3)Hallo, FerrisUnter Windows: Dort ist echo kein eigenes Programm, sondern in die Shell cmd eingebaut, und sh gibt es meist nicht. Dieselbe Idee sieht dann so aus: Command::new("cmd").args(["/C", "echo Hallo"]). Die Beispiele oben funktionieren unter Linux, macOS und im Rust Playground.
Alles zusammen: Konfiguration aus der Umgebung
Abschnitt betitelt „Alles zusammen: Konfiguration aus der Umgebung“Ein echtes Programm liest seine Einstellungen meist einmal beim Start in ein Struct ein. Jede Einstellung hat einen sinnvollen Standardwert, und ein falscher Wert führt zu einer klaren Meldung statt zu einem Absturz:
use std::env;
#[derive(Debug)]struct Config { host: String, port: u16, verbose: bool,}
impl Config { fn from_env() -> Config { let host = env::var("APP_HOST").unwrap_or_else(|_| String::from("127.0.0.1"));
let port = match env::var("APP_PORT") { Ok(text) => text.parse().unwrap_or_else(|_| { println!("APP_PORT={text:?} ist keine Zahl, nutze 3000"); 3000 }), Err(_) => 3000, };
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose } }}
fn main() { // Simuliert, was ein Benutzer vor dem Start setzen könnte. unsafe { env::set_var("APP_PORT", "eighty"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}Ausgabe:
APP_PORT="eighty" ist keine Zahl, nutze 3000Config { host: "127.0.0.1", port: 3000, verbose: true }Im echten Einsatz löschst du die set_var-Zeilen und setzt die Variablen stattdessen in deiner Shell:
APP_PORT=9000 APP_VERBOSE=1 cargo run(In der Windows PowerShell: $env:APP_PORT="9000"; cargo run.)
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“env::var so behandeln, als gäbe es einen String zurück. Es gibt ein Result zurück, weil die Variable vielleicht nicht existiert:
let port: String = env::var("PORT");error[E0308]: mismatched types --> src/main.rs:4:24 |4 | let port: String = env::var("PORT"); | ------ ^^^^^^^^^^^^^^^^ expected `String`, found `Result<String, VarError>` | | | expected due to thisDer Compiler meldet unpassende Typen: Erwartet war ein String, gefunden hat er ein Result<String, VarError>. Die Lösung: Entscheide, was eine fehlende Variable bedeuten soll. env::var("PORT").unwrap_or(String::from("8080")) für einen Standardwert, oder match, um den Fall zu melden.
set_var in Edition 2024 ohne unsafe aufrufen. Neue Projekte, die du mit cargo new anlegst, nutzen Edition 2024. Deshalb scheitert das hier:
error[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block --> src/main.rs:2:5 |2 | std::env::set_var("MODE", "fast"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe functionDer Compiler sagt: Der Aufruf einer unsicheren Funktion braucht einen unsafe-Block. Die Lösung: Pack den Aufruf in unsafe { ... } und ruf ihn nur auf, bevor Threads starten – oder nimm Command::env für Kindprogramme.
Die ganze Kommandozeile in Command::new schreiben. Command::new("echo hello") sucht nach einem Programm, das wörtlich echo hello heißt – und das gibt es nicht. .output() liefert dann Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) („Datei oder Verzeichnis nicht gefunden“). Die Lösung: Der Programmname kommt in new, jedes Argument in .arg() oder .args(): Command::new("echo").arg("hello").
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“env::var("NAME")gibtResult<String, VarError>zurück. Behandle den fehlenden Fall mitmatch,.unwrap_or()oder.ok().- Werte aus der Umgebung sind Text. Zahlen parst du selbst und greifst auf einen Standardwert zurück, wenn das Parsen scheitert.
env::set_varist in Edition 2024unsafe, weil sich alle Threads die Umgebung teilen. Für Kindprogramme istCommand::envdie bessere Wahl.process::exit(code)beendet das Programm sofort mit einem Exit-Code:0heißt Erfolg, alles andere Fehler.Command::new("programm").args([...])startet ein anderes Programm..output()fängt seine Ausgabe ein,.status()liefert dir seinen Exit-Code.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was gibt env::var("PORT") zurück, wenn PORT nicht gesetzt ist?
2. Warum ist env::set_var in Edition 2024 eine unsafe-Funktion?
3. Welcher Exit-Code bedeutet traditionell „Erfolg“?
4. Wie startest du ls -l mit Command?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- std::env – Rust-Standardbibliothek (auf Englisch): alle Funktionen für Variablen, Argumente und Verzeichnisse.
- Mit Umgebungsvariablen arbeiten – Das Rust-Buch: Das Projekt im Buch liest eine Einstellung aus der Umgebung.
- std::process::Command (auf Englisch): alle Optionen zum Starten von Programmen, mit Beispielen für Windows und Unix.
- Newly unsafe functions – Edition Guide (auf Englisch): die offizielle Erklärung, warum
set_varunsafewurde. - Child processes – Rust by Example (auf Englisch): weitere kurze Beispiele zum Starten von Programmen und zu Pipes.