Variabili d'ambiente e processi in Rust
Ogni programma gira dentro un ambiente. Il sistema operativo gli consegna un insieme di valori di testo con un nome, come PATH, HOME o PORT. Sono le variabili d’ambiente. Sono un modo semplice per configurare un programma senza cambiarne il codice: lo stesso binario può usare la porta 8080 sul tuo portatile e la porta 80 su un server.
Un programma vive anche in mezzo ad altri programmi. Può terminare con un codice di uscita che dice a chi l’ha avviato «è andato tutto bene» oppure «qualcosa è fallito», e può avviare a sua volta altri programmi. In Rust, gli strumenti per tutto questo stanno in due moduli standard: std::env e std::process. Non servono crate.
Leggere una variabile con env::var
Sezione intitolata “Leggere una variabile con env::var”std::env::var prende un nome e restituisce un Result. È Ok(String) quando la variabile esiste, ed Err quando manca (o non è Unicode valido). Siccome una variabile mancante è una situazione normale, Rust ti obbliga a gestirla:
use std::env;
fn main() { // Un nome che quasi certamente non è impostato su nessun computer. match env::var("RUSTTUTORIAL_API_KEY") { Ok(value) => println!("Trovata: {value}"), Err(e) => println!("Non disponibile: {e}"), }
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080")); println!("Porta: {port}");}Output:
Non disponibile: environment variable not foundPorta: 8080Il messaggio environment variable not found («variabile d’ambiente non trovata») viene dalla libreria standard ed è in inglese. Usa match quando vuoi reagire al caso della variabile mancante. Usa .unwrap_or(default) quando una variabile mancante vuol dire semplicemente «usa il valore predefinito».
Valori predefiniti, numeri e interruttori
Sezione intitolata “Valori predefiniti, numeri e interruttori”Le variabili d’ambiente sono sempre testo. Per avere un numero devi convertirlo, e anche la conversione può fallire. Uno schema comune è: trasformare il Result in un Option con .ok(), provare a convertire e ripiegare su un valore predefinito se qualcosa va storto:
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}");}Output:
workers = 4debug = false.is_ok() è un modo veloce per trattare una variabile come un interruttore acceso/spento: se è impostata, con qualsiasi valore, l’interruttore è acceso.
var_os, vars e impostare variabili
Sezione intitolata “var_os, vars e impostare variabili”env::var_os restituisce Option<OsString> invece di un Result. Non fallisce mai per byte strani, quindi è comodo per i percorsi. env::vars() ti dà tutte le variabili come coppie (nome, valore).
env::set_var cambia una variabile per il processo corrente. Nota il blocco unsafe intorno:
use std::env;
fn main() { unsafe { env::set_var("MYAPP_COLOR", "blu"); env::set_var("MYAPP_SIZE", "grande"); }
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(); // l'ordine di env::vars() non è garantito
for (key, value) in &mine { println!("{key}={value}"); }}Output:
Some("blu")NoneMYAPP_COLOR=bluMYAPP_SIZE=grandePerché set_var è unsafe? Su molti sistemi operativi l’ambiente è un’unica tabella condivisa da tutto il processo. Se un thread la modifica mentre un altro thread (o una libreria C) la legge, il programma può andare in crash. Rust non può controllarlo per te, quindi dall’edizione 2024 set_var e remove_var sono funzioni unsafe. Nell’edizione 2021 compilano ancora senza unsafe, ma scrivere comunque il blocco funziona in entrambe. La regola sicura: imposta le variabili solo all’inizio di main, prima di avviare qualsiasi thread. Meglio ancora, non cambiare affatto il tuo ambiente. Per dare una variabile a un programma figlio, usa Command::env (lo vedi più sotto). Vedi Rust unsafe per capire cosa vuol dire unsafe.
La cartella corrente
Sezione intitolata “La cartella corrente”env::current_dir() restituisce la cartella da cui è stato avviato il programma, come PathBuf. I percorsi relativi come "config.toml" partono da lì. Il percorso reale dipende dal tuo computer, quindi questo esempio stampa solo cose vere ovunque:
use std::env;
fn main() { let dir = env::current_dir().expect("impossibile leggere la cartella corrente"); println!("Il percorso è assoluto? {}", dir.is_absolute());
let config = dir.join("config.toml"); println!("Finisce con config.toml? {}", config.ends_with("config.toml"));}Output:
Il percorso è assoluto? trueFinisce con config.toml? trueStampa dir.display() sul tuo computer per vedere il percorso completo. Anche gli argomenti della riga di comando stanno in std::env, e hanno una pagina tutta loro: Argomenti da riga di comando.
Codici di uscita con process::exit
Sezione intitolata “Codici di uscita con process::exit”Quando un programma finisce, restituisce un numero a chi l’ha avviato. 0 vuol dire successo; qualsiasi altro valore vuol dire fallimento. Gli script della shell e i sistemi di CI controllano questo numero. std::process::exit(code) ferma subito il programma con quel codice:
use std::env;use std::process;
fn main() { println!("Avvio in corso...");
let token = match env::var("RUSTTUTORIAL_TOKEN") { Ok(t) => t, Err(_) => { eprintln!("errore: RUSTTUTORIAL_TOKEN non è impostata"); process::exit(1); } };
println!("Uso il token {token}");}Output:
Avvio in corso...La riga di errore viene stampata con eprintln!, quindi va su stderr, non su stdout. Il Playground la mostra in un’area separata, insieme allo stato di uscita.
Due cose da sapere. Primo: process::exit termina subito il programma, quindi i distruttori (Drop) dei valori ancora vivi non vengono eseguiti. Secondo: spesso non ti serve: se main restituisce Result<(), E> e tu restituisci Err, Rust stampa l’errore ed esce con il codice 1 al posto tuo.
Eseguire altri programmi con Command
Sezione intitolata “Eseguire altri programmi con Command”std::process::Command avvia un altro programma. Lo costruisci un passo alla volta: il nome del programma, poi ogni argomento con .arg() o .args(). Poi scegli come eseguirlo:
.output()aspetta che finisca e cattura il suo stdout e il suo stderr..status()aspetta che finisca, ma lascia che il suo output vada dritto al tuo terminale.
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("Ciao") .arg("da un processo figlio") .output() .expect("impossibile eseguire echo");
let text = String::from_utf8_lossy(&output.stdout); println!("successo: {}", output.status.success()); println!("stdout: {}", text.trim());}Output:
successo: truestdout: Ciao da un processo figliooutput.stdout sono byte grezzi (Vec<u8>), perché un programma può stampare qualsiasi cosa. String::from_utf8_lossy li trasforma in testo.
Ecco .status() che legge un codice di uscita, e .env() che passa una variabile solo al processo figlio:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "exit 3"]) .status() .expect("impossibile eseguire sh");
println!("successo: {}", status.success()); println!("codice: {:?}", status.code());
let output = Command::new("sh") .args(["-c", "echo \"$GREETING, $NAME\""]) .env("GREETING", "Ciao") .env("NAME", "Ferris") .output() .expect("impossibile eseguire sh"); print!("{}", String::from_utf8_lossy(&output.stdout));}Output:
successo: falsecodice: Some(3)Ciao, FerrisSu Windows: lì echo non è un programma separato, ma un comando interno della shell cmd, e di solito sh non esiste. La stessa idea si scrive Command::new("cmd").args(["/C", "echo Hello"]). Gli esempi qui sopra funzionano su Linux, macOS e nel Rust Playground.
Mettere tutto insieme: la configurazione dall’ambiente
Sezione intitolata “Mettere tutto insieme: la configurazione dall’ambiente”Un programma vero di solito legge tutte le sue impostazioni una sola volta, all’avvio, dentro una struct. Ogni impostazione ha un valore predefinito sensato, e un valore sbagliato produce un messaggio chiaro invece di un crash:
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:?} non è un numero, uso 3000"); 3000 }), Err(_) => 3000, };
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose } }}
fn main() { // Simula quello che un utente potrebbe impostare prima di avviare il programma. unsafe { env::set_var("APP_PORT", "ottanta"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}Output:
APP_PORT="ottanta" non è un numero, uso 3000Config { host: "127.0.0.1", port: 3000, verbose: true }Nell’uso reale, cancella le righe con set_var e imposta invece le variabili nella tua shell:
APP_PORT=9000 APP_VERBOSE=1 cargo run(In Windows PowerShell: $env:APP_PORT="9000"; cargo run.)
Errori comuni
Sezione intitolata “Errori comuni”Trattare env::var come se restituisse una String. Restituisce un Result, perché la variabile potrebbe non esistere:
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 thisIl compilatore dice (in inglese) che si aspettava una String e ha trovato un Result<String, VarError>. La correzione: decidi cosa vuol dire una variabile mancante. env::var("PORT").unwrap_or(String::from("8080")) per un valore predefinito, oppure match per segnalarla.
Chiamare set_var senza unsafe nell’edizione 2024. I nuovi progetti creati con cargo new usano l’edizione 2024, quindi questo fallisce:
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 functionQui il messaggio dice che la chiamata alla funzione unsafe set_var richiede un blocco unsafe. La correzione: avvolgila in unsafe { ... } e chiamala solo prima che parta qualsiasi thread, oppure usa Command::env per i programmi figli.
Mettere tutta la riga di comando in Command::new. Command::new("echo hello") cerca un programma che si chiama letteralmente echo hello, che non esiste. .output() restituisce Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) (nessun file o cartella con quel nome). La correzione: il nome del programma va in new, e ogni argomento va in .arg() o .args(): Command::new("echo").arg("hello").
Riepilogo
Sezione intitolata “Riepilogo”env::var("NOME")restituisceResult<String, VarError>. Gestisci il caso mancante conmatch,.unwrap_or()o.ok().- I valori dell’ambiente sono testo. Converti tu i numeri e ripiega su un valore predefinito quando la conversione fallisce.
env::set_varèunsafenell’edizione 2024 perché l’ambiente è condiviso da tutti i thread. Per i programmi figli preferisciCommand::env.process::exit(code)termina subito il programma con un codice di uscita:0è successo, qualsiasi altro valore è fallimento.Command::new("programma").args([...])esegue un altro programma..output()cattura quello che stampa;.status()ti dà il suo codice di uscita.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa restituisce env::var("PORT") quando PORT non è impostata?
2. Perché env::set_var è una funzione unsafe nell'edizione 2024?
3. Quale codice di uscita indica per tradizione «successo»?
4. Come esegui ls -l con Command?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- std::env — libreria standard di Rust (in inglese): tutte le funzioni per variabili, argomenti e cartelle.
- Working with Environment Variables — The Rust Book (in inglese): il progetto del Book che legge un’impostazione dall’ambiente.
- std::process::Command (in inglese): tutte le opzioni per avviare programmi, con esempi per Windows e Unix.
- Newly unsafe functions — Edition Guide (in inglese): la spiegazione ufficiale del perché
set_varè diventataunsafe. - Child processes — Rust by Example (in inglese): altri brevi esempi di come eseguire programmi e usare le pipe.