Pular para o conteúdo

Variáveis de ambiente e processos em Rust

Todo programa roda dentro de um ambiente. O sistema operacional entrega a ele um conjunto de valores de texto com nome, como PATH, HOME ou PORT. Essas são as variáveis de ambiente. Elas são um jeito simples de configurar um programa sem mexer no código: o mesmo binário pode usar a porta 8080 no seu notebook e a porta 80 em um servidor.

Um programa também convive com outros programas. Ele pode terminar com um código de saída (exit code) que diz para quem o chamou “deu certo” ou “deu errado”, e ele mesmo pode iniciar outros programas. Em Rust, as ferramentas para tudo isso ficam em dois módulos da biblioteca padrão: std::env e std::process. Nenhum crate é necessário.

std::env::var recebe um nome e devolve um Result. Ele é Ok(String) quando a variável existe e Err quando ela não existe (ou não é Unicode válido). Como uma variável ausente é uma situação normal, o Rust obriga você a tratar esse caso:

use std::env;
fn main() {
// Um nome que quase com certeza não está definido em nenhuma máquina.
match env::var("RUSTTUTORIAL_API_KEY") {
Ok(value) => println!("Encontrei: {value}"),
Err(e) => println!("Indisponível: {e}"),
}
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080"));
println!("Porta: {port}");
}

Saída:

Indisponível: environment variable not found
Porta: 8080
▶ Experimente você mesmo

A mensagem “environment variable not found” (variável de ambiente não encontrada) vem do próprio Rust, por isso aparece em inglês. Use match quando quiser reagir ao caso da variável ausente. Use .unwrap_or(padrão) quando uma variável ausente simplesmente quer dizer “use o valor padrão”.

Variáveis de ambiente são sempre texto. Para ter um número, você precisa convertê-lo, e a conversão também pode falhar. Um padrão comum é: transformar o Result em um Option com .ok(), tentar converter e cair em um valor padrão se qualquer coisa der errado:

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

Saída:

workers = 4
debug = false
▶ Experimente você mesmo

.is_ok() é um jeito rápido de tratar uma variável como uma flag de liga/desliga: se ela estiver definida, com qualquer valor, a flag está ligada.

env::var_os devolve Option<OsString> em vez de um Result. Ele nunca falha por causa de bytes estranhos, então é útil para caminhos de arquivo. env::vars() entrega todas as variáveis como pares (nome, valor).

env::set_var muda uma variável para o processo atual. Repare no bloco unsafe em volta dele:

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(); // a ordem de env::vars() não é garantida
for (key, value) in &mine {
println!("{key}={value}");
}
}

Saída:

Some("azul")
None
MYAPP_COLOR=azul
MYAPP_SIZE=grande
▶ Experimente você mesmo

Por que set_var é unsafe? Em muitos sistemas operacionais, o ambiente é uma única tabela compartilhada pelo processo inteiro. Se uma thread muda essa tabela enquanto outra thread (ou uma biblioteca em C) está lendo, o programa pode travar. O Rust não consegue verificar isso por você, então, desde a edição 2024, set_var e remove_var são funções unsafe. Na edição 2021 elas ainda compilam sem unsafe, mas escrever o bloco mesmo assim funciona nas duas. A regra segura: só defina variáveis bem no começo do main, antes de iniciar qualquer thread. Melhor ainda: não mude o seu próprio ambiente. Para passar uma variável a um programa filho, use Command::env (mostrado mais abaixo). Veja Unsafe Rust para entender o que unsafe significa.

env::current_dir() devolve a pasta de onde o programa foi iniciado, como um PathBuf. Caminhos relativos como "config.toml" são resolvidos a partir dela. O caminho real depende da sua máquina, então este exemplo só imprime coisas que são verdade em qualquer lugar:

use std::env;
fn main() {
let dir = env::current_dir().expect("não foi possível ler o diretório atual");
println!("O caminho é absoluto? {}", dir.is_absolute());
let config = dir.join("config.toml");
println!("Termina com config.toml? {}", config.ends_with("config.toml"));
}

Saída:

O caminho é absoluto? true
Termina com config.toml? true
▶ Experimente você mesmo

No seu computador, imprima dir.display() para ver o caminho completo. Os argumentos de linha de comando também ficam em std::env, e eles têm uma página só deles: Argumentos de linha de comando.

Quando um programa termina, ele devolve um número para quem o iniciou. 0 quer dizer sucesso; qualquer outro valor quer dizer falha. Scripts de shell e sistemas de CI verificam esse número. std::process::exit(code) encerra o programa na hora, com esse código:

use std::env;
use std::process;
fn main() {
println!("Iniciando...");
let token = match env::var("RUSTTUTORIAL_TOKEN") {
Ok(t) => t,
Err(_) => {
eprintln!("erro: RUSTTUTORIAL_TOKEN não está definida");
process::exit(1);
}
};
println!("Usando o token {token}");
}

Saída:

Iniciando...
▶ Experimente você mesmo

A linha de erro é impressa com eprintln!, então ela vai para o stderr, não para o stdout. O Playground mostra essa linha em uma área separada, junto com o status de saída.

Duas coisas para saber. Primeiro, process::exit encerra o programa imediatamente, então os destrutores (Drop) dos valores que ainda estão vivos não rodam. Segundo, muitas vezes você nem precisa dele: se o main devolve Result<(), E> e você devolve Err, o Rust imprime o erro e sai com o código 1 por você.

std::process::Command inicia outro programa. Você monta o comando passo a passo: o nome do programa e depois cada argumento com .arg() ou .args(). Aí você escolhe como rodar:

  • .output() espera o programa terminar e captura o stdout e o stderr dele.
  • .status() também espera, mas deixa a saída do programa ir direto para o seu terminal.
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("Olá")
.arg("de um processo filho")
.output()
.expect("falha ao rodar o echo");
let text = String::from_utf8_lossy(&output.stdout);
println!("sucesso: {}", output.status.success());
println!("stdout: {}", text.trim());
}

Saída:

sucesso: true
stdout: Olá de um processo filho
▶ Experimente você mesmo

output.stdout são bytes crus (Vec<u8>), porque um programa pode imprimir qualquer coisa. String::from_utf8_lossy transforma esses bytes em texto.

Aqui está o .status() lendo um código de saída, e o .env() passando uma variável só para o processo filho:

use std::process::Command;
fn main() {
let status = Command::new("sh")
.args(["-c", "exit 3"])
.status()
.expect("falha ao rodar o sh");
println!("sucesso: {}", status.success());
println!("código: {:?}", status.code());
let output = Command::new("sh")
.args(["-c", "echo \"$GREETING, $NAME\""])
.env("GREETING", "Oi")
.env("NAME", "Ferris")
.output()
.expect("falha ao rodar o sh");
print!("{}", String::from_utf8_lossy(&output.stdout));
}

Saída:

sucesso: false
código: Some(3)
Oi, Ferris
▶ Experimente você mesmo

No Windows: lá o echo não é um programa separado; ele faz parte do shell cmd, e o sh normalmente não existe. A mesma ideia fica assim: Command::new("cmd").args(["/C", "echo Olá"]). Os exemplos acima funcionam no Linux, no macOS e no Rust Playground.

Juntando tudo: configuração a partir do ambiente

Seção intitulada “Juntando tudo: configuração a partir do ambiente”

Um programa de verdade costuma ler todas as configurações uma vez só, na inicialização, para dentro de uma struct. Cada configuração tem um valor padrão razoável, e um valor ruim gera uma mensagem clara em vez de derrubar o programa:

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ão é um número, usando 3000");
3000
}),
Err(_) => 3000,
};
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose }
}
}
fn main() {
// Simula o que um usuário poderia definir antes de rodar o programa.
unsafe {
env::set_var("APP_PORT", "oitenta");
env::set_var("APP_VERBOSE", "true");
}
let config = Config::from_env();
println!("{config:?}");
}

Saída:

APP_PORT="oitenta" não é um número, usando 3000
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ Experimente você mesmo

No uso real, apague as linhas com set_var e defina as variáveis no seu shell:

Terminal window
APP_PORT=9000 APP_VERBOSE=1 cargo run

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

Tratar env::var como se ele devolvesse uma String. Ele devolve um Result, porque a variável pode não 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

O compilador diz que os tipos não batem: ele esperava uma String e encontrou um Result<String, VarError>. A solução: decida o que uma variável ausente significa. Use env::var("PORT").unwrap_or(String::from("8080")) para ter um valor padrão, ou match para avisar o usuário.

Chamar set_var sem unsafe na edição 2024. Projetos novos criados com cargo new usam a edição 2024, então isto falha:

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

O erro diz que chamar a função set_var é unsafe e exige um bloco unsafe. A solução: coloque a chamada dentro de unsafe { ... } e só faça isso antes de iniciar qualquer thread, ou use Command::env para programas filhos.

Colocar a linha de comando inteira em Command::new. Command::new("echo hello") procura um programa chamado literalmente echo hello, que não existe. O .output() devolve Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) (arquivo ou diretório não encontrado). A solução: o nome do programa vai no new, e cada argumento vai em .arg() ou .args(): Command::new("echo").arg("hello").

  • env::var("NAME") devolve Result<String, VarError>. Trate o caso da variável ausente com match, .unwrap_or() ou .ok().
  • Os valores do ambiente são texto. Converta os números você mesmo e caia em um valor padrão quando a conversão falhar.
  • env::set_var é unsafe na edição 2024 porque o ambiente é compartilhado por todas as threads. Prefira Command::env para programas filhos.
  • process::exit(code) encerra o programa na hora com um código de saída: 0 é sucesso, qualquer outro valor é falha.
  • Command::new("programa").args([...]) roda outro programa. .output() captura o que ele imprime; .status() dá o código de saída dele.

Teste o que aprendeu

1. O que env::var("PORT") devolve quando PORT não está definida?

2. Por que env::set_var é uma função unsafe na edição 2024?

3. Qual código de saída tradicionalmente significa "sucesso"?

4. Como você roda ls -l com Command?

Pontuação: 0 / 4