Aller au contenu

Variables d'environnement et processus en Rust

Tout programme s’exécute dans un environnement. Le système d’exploitation lui transmet un ensemble de valeurs texte nommées, comme PATH, HOME ou PORT. Ce sont les variables d’environnement. Elles offrent un moyen simple de configurer un programme sans toucher à son code : le même binaire peut utiliser le port 8080 sur votre ordinateur portable et le port 80 sur un serveur.

Un programme vit aussi au milieu d’autres programmes. Il peut se terminer avec un code de sortie qui dit à celui qui l’a lancé « ça a marché » ou « ça a échoué », et il peut lui-même lancer d’autres programmes. En Rust, les outils pour tout cela se trouvent dans deux modules standard : std::env et std::process. Aucune crate n’est nécessaire.

std::env::var prend un nom et renvoie un Result. C’est Ok(String) quand la variable existe, et Err quand elle est absente (ou que ce n’est pas de l’Unicode valide). Comme une variable absente est une situation normale, Rust vous oblige à gérer ce cas :

use std::env;
fn main() {
// Un nom qui n'est presque certainement défini sur aucune machine.
match env::var("RUSTTUTORIAL_API_KEY") {
Ok(value) => println!("Trouvée : {value}"),
Err(e) => println!("Indisponible : {e}"),
}
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080"));
println!("Port : {port}");
}

Sortie :

Indisponible : environment variable not found
Port : 8080
▶ Essayez par vous-même

Le message « environment variable not found » vient de Rust lui-même : il signifie que la variable d’environnement est introuvable. Utilisez match quand vous voulez réagir au cas où elle manque. Utilisez .unwrap_or(default) quand une variable absente veut simplement dire « prends la valeur par défaut ».

Les variables d’environnement sont toujours du texte. Pour obtenir un nombre, il faut le convertir, et la conversion peut échouer elle aussi. Un schéma courant : transformer le Result en Option avec .ok(), tenter la conversion, et revenir à une valeur par défaut si quelque chose se passe mal :

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}");
}

Sortie :

workers = 4
debug = false
▶ Essayez par vous-même

.is_ok() est un moyen rapide de traiter une variable comme un interrupteur (flag) : si elle est définie, quelle que soit sa valeur, le flag est activé.

env::var_os renvoie une Option<OsString> au lieu d’un Result. Elle n’échoue jamais sur des octets inhabituels, ce qui la rend pratique pour les chemins. env::vars() vous donne toutes les variables sous forme de paires (nom, valeur).

env::set_var modifie une variable pour le processus en cours. Remarquez le bloc unsafe autour :

use std::env;
fn main() {
unsafe {
env::set_var("MYAPP_COLOR", "bleu");
env::set_var("MYAPP_SIZE", "grand");
}
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'ordre de env::vars() n'est pas garanti
for (key, value) in &mine {
println!("{key}={value}");
}
}

Sortie :

Some("bleu")
None
MYAPP_COLOR=bleu
MYAPP_SIZE=grand
▶ Essayez par vous-même

Pourquoi set_var est-elle unsafe ? Sur beaucoup de systèmes d’exploitation, l’environnement est une seule table partagée par tout le processus. Si un thread la modifie pendant qu’un autre thread (ou une bibliothèque C) la lit, le programme peut planter. Rust ne peut pas vérifier ça pour vous : depuis l’édition 2024, set_var et remove_var sont donc des fonctions unsafe. En édition 2021, elles compilent encore sans unsafe, mais écrire le bloc quand même fonctionne dans les deux cas. La règle sûre : ne définissez des variables qu’au tout début de main, avant de lancer le moindre thread. Mieux encore, ne modifiez pas du tout votre propre environnement. Pour transmettre une variable à un programme enfant, utilisez Command::env (présenté plus bas). Voir Unsafe en Rust pour comprendre ce que signifie unsafe.

env::current_dir() renvoie le dossier depuis lequel le programme a été lancé, sous forme de PathBuf. Les chemins relatifs comme "config.toml" sont résolus par rapport à lui. Le vrai chemin dépend de votre machine, donc cet exemple n’affiche que des faits vrais partout :

use std::env;
fn main() {
let dir = env::current_dir().expect("impossible de lire le dossier courant");
println!("Le chemin est-il absolu ? {}", dir.is_absolute());
let config = dir.join("config.toml");
println!("Se termine par config.toml ? {}", config.ends_with("config.toml"));
}

Sortie :

Le chemin est-il absolu ? true
Se termine par config.toml ? true
▶ Essayez par vous-même

Affichez dir.display() sur votre machine pour voir le chemin complet. Les arguments de ligne de commande se trouvent aussi dans std::env ; ils ont leur propre page : Arguments de ligne de commande.

Quand un programme se termine, il renvoie un nombre à celui qui l’a lancé. 0 signifie succès ; toute autre valeur signifie échec. Les scripts shell et les systèmes de CI vérifient ce nombre. std::process::exit(code) arrête le programme immédiatement avec ce code :

use std::env;
use std::process;
fn main() {
println!("Démarrage...");
let token = match env::var("RUSTTUTORIAL_TOKEN") {
Ok(t) => t,
Err(_) => {
eprintln!("erreur : RUSTTUTORIAL_TOKEN n'est pas définie");
process::exit(1);
}
};
println!("Utilisation du jeton {token}");
}

Sortie :

Démarrage...
▶ Essayez par vous-même

La ligne d’erreur est affichée avec eprintln! : elle part donc sur stderr, pas sur stdout. Le Playground l’affiche dans une zone séparée, avec le code de sortie.

Deux choses à savoir. D’abord, process::exit termine le programme sur-le-champ : les destructeurs (Drop) des valeurs encore vivantes ne s’exécutent pas. Ensuite, vous n’en avez souvent pas besoin : si main renvoie Result<(), E> et que vous renvoyez Err, Rust affiche l’erreur et quitte avec le code 1 pour vous.

std::process::Command lance un autre programme. Vous le construisez étape par étape : le nom du programme, puis chaque argument avec .arg() ou .args(). Ensuite, vous choisissez comment l’exécuter :

  • .output() attend qu’il se termine et capture son stdout et son stderr.
  • .status() attend aussi qu’il se termine, mais laisse sa sortie aller directement dans votre terminal.
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("Bonjour")
.arg("depuis un processus enfant")
.output()
.expect("impossible de lancer echo");
let text = String::from_utf8_lossy(&output.stdout);
println!("succès : {}", output.status.success());
println!("stdout : {}", text.trim());
}

Sortie :

succès : true
stdout : Bonjour depuis un processus enfant
▶ Essayez par vous-même

output.stdout contient des octets bruts (Vec<u8>), car un programme peut afficher n’importe quoi. String::from_utf8_lossy les transforme en texte.

Voici .status() qui lit un code de sortie, et .env() qui transmet une variable au programme enfant uniquement :

use std::process::Command;
fn main() {
let status = Command::new("sh")
.args(["-c", "exit 3"])
.status()
.expect("impossible de lancer sh");
println!("succès : {}", status.success());
println!("code : {:?}", status.code());
let output = Command::new("sh")
.args(["-c", "echo \"$GREETING, $NAME\""])
.env("GREETING", "Salut")
.env("NAME", "Ferris")
.output()
.expect("impossible de lancer sh");
print!("{}", String::from_utf8_lossy(&output.stdout));
}

Sortie :

succès : false
code : Some(3)
Salut, Ferris
▶ Essayez par vous-même

Sous Windows : echo n’y est pas un programme séparé ; il est intégré au shell cmd, et sh n’existe généralement pas. La même idée s’écrit Command::new("cmd").args(["/C", "echo Bonjour"]). Les exemples ci-dessus fonctionnent sous Linux, macOS et dans le Rust Playground.

Tout assembler : une configuration lue dans l’environnement

Section intitulée « Tout assembler : une configuration lue dans l’environnement »

Un vrai programme lit en général tous ses réglages une seule fois, au démarrage, dans une struct. Chaque réglage a une valeur par défaut raisonnable, et une valeur invalide donne un message clair au lieu d’un plantage :

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:?} n'est pas un nombre, on utilise 3000");
3000
}),
Err(_) => 3000,
};
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose }
}
}
fn main() {
// On simule ce qu'un utilisateur pourrait définir avant de lancer le programme.
unsafe {
env::set_var("APP_PORT", "quatre-vingts");
env::set_var("APP_VERBOSE", "true");
}
let config = Config::from_env();
println!("{config:?}");
}

Sortie :

APP_PORT="quatre-vingts" n'est pas un nombre, on utilise 3000
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ Essayez par vous-même

En usage réel, supprimez les lignes set_var et définissez plutôt les variables dans votre shell :

Terminal window
APP_PORT=9000 APP_VERBOSE=1 cargo run

(Sous Windows PowerShell : $env:APP_PORT="9000"; cargo run.)

Traiter env::var comme si elle renvoyait une String. Elle renvoie un Result, car la variable n’existe peut-être pas :

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 this

Le compilateur signale des types incompatibles : il attendait une String et a trouvé un Result<String, VarError>. La solution : décidez ce que signifie une variable absente. env::var("PORT").unwrap_or(String::from("8080")) pour une valeur par défaut, ou match pour signaler le problème.

Appeler set_var sans unsafe en édition 2024. Les nouveaux projets créés avec cargo new utilisent l’édition 2024, donc ceci échoue :

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 function

Le compilateur explique que l’appel à la fonction unsafe set_var exige un bloc unsafe. La solution : entourez-la de unsafe { ... } et appelez-la uniquement avant le lancement de tout thread, ou utilisez Command::env pour les programmes enfants.

Mettre toute la ligne de commande dans Command::new. Command::new("echo hello") cherche un programme nommé littéralement echo hello, qui n’existe pas. .output() renvoie Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) (fichier ou dossier introuvable). La solution : le nom du programme va dans new, et chaque argument dans .arg() ou .args() : Command::new("echo").arg("hello").

  • env::var("NOM") renvoie Result<String, VarError>. Gérez le cas où la variable manque avec match, .unwrap_or() ou .ok().
  • Les valeurs d’environnement sont du texte. Convertissez vous-même les nombres et revenez à une valeur par défaut si la conversion échoue.
  • env::set_var est unsafe en édition 2024, car l’environnement est partagé par tous les threads. Préférez Command::env pour les programmes enfants.
  • process::exit(code) termine le programme sur-le-champ avec un code de sortie : 0 pour un succès, toute autre valeur pour un échec.
  • Command::new("programme").args([...]) lance un autre programme. .output() capture ce qu’il affiche ; .status() vous donne son code de sortie.

Petit quiz

1. Que renvoie env::var("PORT") quand PORT n'est pas définie ?

2. Pourquoi env::set_var est-elle une fonction unsafe en édition 2024 ?

3. Quel code de sortie signifie traditionnellement « succès » ?

4. Comment lancer ls -l avec Command ?

Score : 0 / 4