Variables de entorno y procesos en Rust
Todo programa se ejecuta dentro de un entorno. El sistema operativo le entrega un conjunto de valores de texto con nombre, como PATH, HOME o PORT. Son las variables de entorno. Son una forma sencilla de configurar un programa sin cambiar su código: el mismo binario puede usar el puerto 8080 en tu portátil y el puerto 80 en un servidor.
Un programa además convive con otros programas. Puede terminar con un código de salida que le dice a quien lo lanzó “todo fue bien” o “algo falló”, y también puede iniciar otros programas. En Rust, las herramientas para todo esto están en dos módulos estándar: std::env y std::process. No hace falta ningún crate.
Leer una variable con env::var
Sección titulada «Leer una variable con env::var»std::env::var recibe un nombre y devuelve un Result. Es Ok(String) cuando la variable existe, y Err cuando falta (o no es Unicode válido). Como que falte una variable es una situación normal, Rust te obliga a tratar ese caso:
use std::env;
fn main() { // Un nombre que casi seguro no está definido en ningún equipo. match env::var("RUSTTUTORIAL_API_KEY") { Ok(value) => println!("Encontrada: {value}"), Err(e) => println!("No disponible: {e}"), }
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080")); println!("Puerto: {port}");}Salida:
No disponible: environment variable not foundPuerto: 8080El texto “environment variable not found” (variable de entorno no encontrada) es el mensaje de error de la propia biblioteca estándar, por eso sale en inglés.
Usa match cuando quieras reaccionar al caso en que falta. Usa .unwrap_or(valor_por_defecto) cuando que falte la variable simplemente signifique “usa el valor por defecto”.
Valores por defecto, números y opciones
Sección titulada «Valores por defecto, números y opciones»Las variables de entorno siempre son texto. Para obtener un número, tienes que convertirlo, y la conversión también puede fallar. Un patrón habitual es: convertir el Result en un Option con .ok(), intentar la conversión y usar un valor por defecto si algo sale 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}");}Salida:
workers = 4debug = false.is_ok() es una forma rápida de tratar una variable como una opción de encendido/apagado: si está definida, con el valor que sea, la opción está activada.
var_os, vars y cómo definir variables
Sección titulada «var_os, vars y cómo definir variables»env::var_os devuelve Option<OsString> en lugar de un Result. Nunca falla por bytes raros, así que viene bien para rutas. env::vars() te da todas las variables como pares (nombre, valor).
env::set_var cambia una variable para el proceso actual. Fíjate en el bloque unsafe que la rodea:
use std::env;
fn main() { unsafe { env::set_var("MYAPP_COLOR", "azul"); 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(); // el orden de env::vars() no está garantizado
for (key, value) in &mine { println!("{key}={value}"); }}Salida:
Some("azul")NoneMYAPP_COLOR=azulMYAPP_SIZE=grande¿Por qué set_var es unsafe? En muchos sistemas operativos, el entorno es una única tabla compartida por todo el proceso. Si un hilo la cambia mientras otro hilo (o una biblioteca de C) la lee, el programa puede fallar. Rust no puede comprobar esto por ti, así que desde la edición 2024 set_var y remove_var son funciones unsafe. En la edición 2021 todavía compilan sin unsafe, pero escribir el bloque igualmente funciona en las dos. La regla segura: define variables solo al principio de main, antes de lanzar ningún hilo. Mejor aún, no cambies tu propio entorno. Para pasarle una variable a un programa hijo, usa Command::env (lo verás más abajo). Consulta Unsafe en Rust para saber qué significa unsafe.
El directorio actual
Sección titulada «El directorio actual»env::current_dir() devuelve la carpeta desde la que se lanzó el programa, como un PathBuf. Las rutas relativas como "config.toml" se resuelven a partir de ella. La ruta real depende de tu equipo, así que este ejemplo solo imprime cosas que son ciertas en cualquier sitio:
use std::env;
fn main() { let dir = env::current_dir().expect("no se pudo leer el directorio actual"); println!("¿La ruta es absoluta? {}", dir.is_absolute());
let config = dir.join("config.toml"); println!("¿Termina en config.toml? {}", config.ends_with("config.toml"));}Salida:
¿La ruta es absoluta? true¿Termina en config.toml? trueEn tu equipo, imprime dir.display() para ver la ruta completa. Los argumentos de línea de comandos también están en std::env, y tienen su propia página: Argumentos de línea de comandos.
Códigos de salida con process::exit
Sección titulada «Códigos de salida con process::exit»Cuando un programa termina, le devuelve un número a quien lo lanzó. 0 significa éxito; cualquier otro valor significa fallo. Los scripts de shell y los sistemas de CI comprueban ese número. std::process::exit(code) detiene el programa en el acto con ese código:
use std::env;use std::process;
fn main() { println!("Iniciando...");
let token = match env::var("RUSTTUTORIAL_TOKEN") { Ok(t) => t, Err(_) => { eprintln!("error: RUSTTUTORIAL_TOKEN no está definida"); process::exit(1); } };
println!("Usando el token {token}");}Salida:
Iniciando...La línea de error se imprime con eprintln!, así que va a stderr (la salida de errores), no a stdout. El Playground la muestra en una zona aparte, junto con el estado de salida.
Dos cosas que conviene saber. Primero, process::exit termina el programa inmediatamente, así que los destructores (Drop) de los valores que siguen vivos no se ejecutan. Segundo, muchas veces no lo necesitas: si main devuelve Result<(), E> y devuelves Err, Rust imprime el error y sale con el código 1 por ti.
Ejecutar otros programas con Command
Sección titulada «Ejecutar otros programas con Command»std::process::Command inicia otro programa. Lo construyes paso a paso: el nombre del programa y luego cada argumento con .arg() o .args(). Después eliges cómo ejecutarlo:
.output()espera a que termine y captura su stdout y su stderr..status()espera a que termine, pero deja que su salida vaya directamente a tu terminal.
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("Hola") .arg("desde un proceso hijo") .output() .expect("no se pudo ejecutar echo");
let text = String::from_utf8_lossy(&output.stdout); println!("éxito: {}", output.status.success()); println!("stdout: {}", text.trim());}Salida:
éxito: truestdout: Hola desde un proceso hijooutput.stdout son bytes en bruto (Vec<u8>), porque un programa puede imprimir cualquier cosa. String::from_utf8_lossy los convierte en texto.
Aquí tienes .status() leyendo un código de salida, y .env() pasándole una variable solo al proceso hijo:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "exit 3"]) .status() .expect("no se pudo ejecutar sh");
println!("éxito: {}", status.success()); println!("código: {:?}", status.code());
let output = Command::new("sh") .args(["-c", "echo \"$GREETING, $NAME\""]) .env("GREETING", "Hola") .env("NAME", "Ferris") .output() .expect("no se pudo ejecutar sh"); print!("{}", String::from_utf8_lossy(&output.stdout));}Salida:
éxito: falsecódigo: Some(3)Hola, FerrisEn Windows: allí echo no es un programa aparte; está integrado en la shell cmd, y normalmente sh no existe. La misma idea se escribe así: Command::new("cmd").args(["/C", "echo Hola"]). Los ejemplos de arriba funcionan en Linux, macOS y el Playground de Rust.
Todo junto: configuración desde el entorno
Sección titulada «Todo junto: configuración desde el entorno»Un programa real suele leer toda su configuración una sola vez, al arrancar, y guardarla en un struct. Cada opción tiene un valor por defecto razonable, y un valor incorrecto da un mensaje claro en lugar de hacer que el programa falle:
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:?} no es un número, se usa 3000"); 3000 }), Err(_) => 3000, };
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose } }}
fn main() { // Simulamos lo que un usuario podría definir antes de ejecutar el programa. unsafe { env::set_var("APP_PORT", "ochenta"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}Salida:
APP_PORT="ochenta" no es un número, se usa 3000Config { host: "127.0.0.1", port: 3000, verbose: true }En un uso real, borra las líneas de set_var y define las variables en tu shell:
APP_PORT=9000 APP_VERBOSE=1 cargo run(En Windows PowerShell: $env:APP_PORT="9000"; cargo run.)
Errores comunes
Sección titulada «Errores comunes»Tratar env::var como si devolviera un String. Devuelve un Result, porque la variable podría no existir:
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 thisEl compilador te dice que los tipos no coinciden: esperaba un String y encontró un Result<String, VarError>. La solución: decide qué significa que falte la variable. env::var("PORT").unwrap_or(String::from("8080")) para un valor por defecto, o match para avisar del problema.
Llamar a set_var sin unsafe en la edición 2024. Los proyectos nuevos creados con cargo new usan la edición 2024, así que esto falla:
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 functionEl compilador te dice que set_var es una función unsafe y que llamarla requiere un bloque unsafe. La solución: envuélvela en unsafe { ... } y llámala solo antes de lanzar ningún hilo, o usa Command::env para los programas hijos.
Poner la línea de comandos entera en Command::new. Command::new("echo hello") busca un programa que se llame literalmente echo hello, y ese programa no existe. .output() devuelve Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) (no existe el archivo o directorio). La solución: el nombre del programa va en new, y cada argumento va en .arg() o .args(): Command::new("echo").arg("hello").
Resumen
Sección titulada «Resumen»env::var("NOMBRE")devuelveResult<String, VarError>. Trata el caso en que falta conmatch,.unwrap_or()o.ok().- Los valores del entorno son texto. Convierte tú los números y usa un valor por defecto cuando la conversión falle.
env::set_varesunsafeen la edición 2024 porque todos los hilos comparten el entorno. Para los programas hijos, mejor usaCommand::env.process::exit(code)termina el programa en el acto con un código de salida:0es éxito, cualquier otro valor es fallo.Command::new("programa").args([...])ejecuta otro programa..output()captura lo que imprime;.status()te da su código de salida.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué devuelve env::var("PORT") cuando PORT no está definida?
2. ¿Por qué env::set_var es una función unsafe en la edición 2024?
3. ¿Qué código de salida significa tradicionalmente "éxito"?
4. ¿Cómo ejecutas ls -l con Command?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- std::env — biblioteca estándar de Rust (en inglés): todas las funciones para variables, argumentos y directorios.
- Working with Environment Variables — The Rust Book (en inglés): el proyecto del libro lee una opción de configuración desde el entorno.
- std::process::Command (en inglés): todas las opciones para iniciar programas, con ejemplos para Windows y Unix.
- Newly unsafe functions — Edition Guide (en inglés): la explicación oficial de por qué
set_varpasó a serunsafe. - Child processes — Rust by Example (en inglés): más ejemplos cortos de cómo ejecutar programas y usar pipes.