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.
Lendo uma variável com env::var
Seção intitulada “Lendo uma variável com env::var”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 foundPorta: 8080A 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”.
Valores padrão, números e flags
Seção intitulada “Valores padrão, números e flags”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 = 4debug = false.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.
var_os, vars e definindo variáveis
Seção intitulada “var_os, vars e definindo variáveis”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")NoneMYAPP_COLOR=azulMYAPP_SIZE=grandePor 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.
O diretório atual
Seção intitulada “O diretório atual”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? trueTermina com config.toml? trueNo 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.
Códigos de saída com process::exit
Seção intitulada “Códigos de saída com process::exit”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...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ê.
Rodando outros programas com Command
Seção intitulada “Rodando outros programas com Command”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: truestdout: Olá de um processo filhooutput.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: falsecódigo: Some(3)Oi, FerrisNo 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 3000Config { host: "127.0.0.1", port: 3000, verbose: true }No uso real, apague as linhas com set_var e defina as variáveis no seu shell:
APP_PORT=9000 APP_VERBOSE=1 cargo run(No PowerShell do Windows: $env:APP_PORT="9000"; cargo run.)
Erros comuns
Seção intitulada “Erros comuns”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 thisO 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 functionO 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")devolveResult<String, VarError>. Trate o caso da variável ausente commatch,.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éunsafena edição 2024 porque o ambiente é compartilhado por todas as threads. PrefiraCommand::envpara 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- std::env — biblioteca padrão do Rust (em inglês): todas as funções para variáveis, argumentos e diretórios.
- Working with Environment Variables — The Rust Book (em inglês): o projeto do livro lê uma configuração do ambiente.
- std::process::Command (em inglês): todas as opções para iniciar programas, com exemplos para Windows e Unix.
- Newly unsafe functions — Edition Guide (em inglês): a explicação oficial de por que
set_varvirouunsafe. - Child processes — Rust by Example (em inglês): mais exemplos curtos de como rodar programas e usar pipes.