Ir al contenido

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.

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 found
Puerto: 8080
▶ Pruébalo tú mismo

El 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”.

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 = 4
debug = false
▶ Pruébalo tú mismo

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

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")
None
MYAPP_COLOR=azul
MYAPP_SIZE=grande
▶ Pruébalo tú mismo

¿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.

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? true
▶ Pruébalo tú mismo

En 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.

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...
▶ Pruébalo tú mismo

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.

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: true
stdout: Hola desde un proceso hijo
▶ Pruébalo tú mismo

output.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: false
código: Some(3)
Hola, Ferris
▶ Pruébalo tú mismo

En 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.

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 3000
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ Pruébalo tú mismo

En un uso real, borra las líneas de set_var y define las variables en tu shell:

Terminal window
APP_PORT=9000 APP_VERBOSE=1 cargo run

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

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 this

El 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 function

El 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").

  • env::var("NOMBRE") devuelve Result<String, VarError>. Trata el caso en que falta con match, .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_var es unsafe en la edición 2024 porque todos los hilos comparten el entorno. Para los programas hijos, mejor usa Command::env.
  • process::exit(code) termina el programa en el acto con un código de salida: 0 es é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 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