Introdução aos Processos em Rust
A execução de subprocessos é uma necessidade comum em aplicações modernas. Seja para chamar utilitários do sistema, executar scripts ou integrar ferramentas externas, Rust oferece através do módulo std::process uma API poderosa e segura para esse propósito. Diferentemente de linguagens como C, onde gerenciar processos pode ser perigoso e propenso a erros, Rust fornece abstrações que garantem segurança de memória mesmo ao trabalhar com subprocessos. Nesta aula, vamos explorar como criar, configurar e controlar subprocessos de forma eficiente.
Criando e Executando Subprocessos
Comando Simples com Command
A estrutura fundamental é Command, que representa um processo a ser executado. Para criar um comando básico, usamos o construtor new() passando o executável desejado:
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("Olá, Rust!")
.output()
.expect("Falha ao executar comando");
println!("Status: {}", output.status);
println!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
}
O método output() aguarda a conclusão do processo e retorna um Result<Output> contendo o status, stdout e stderr. Essa é a forma mais simples quando você precisa do resultado completo. Para processos que podem falhar, sempre trate o Result apropriadamente com expect(), unwrap() ou tratamento de erro customizado.
Streaming com spawn()
Quando o processo é longo ou produz muitos dados, usar output() mantém tudo na memória. O método spawn() retorna um Child — uma representação do processo em execução — permitindo trabalhar com streams:
use std::process::{Command, Stdio};
use std::io::{BufRead, BufReader};
fn main() {
let mut child = Command::new("ls")
.arg("-la")
.stdout(Stdio::piped())
.spawn()
.expect("Falha ao iniciar processo");
let stdout = child.stdout.take().expect("Falha ao capturar stdout");
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Ok(line) = line {
println!("{}", line);
}
}
let status = child.wait().expect("Falha ao aguardar processo");
println!("Processo terminou com: {}", status.code().unwrap_or(-1));
}
Aqui usamos Stdio::piped() para capturar a saída padrão. Note que stdout é uma Option que precisa ser extraída com take(). Esse padrão evita que múltiplas partes do código tentem acessar o mesmo recurso simultaneamente, mantendo a segurança.
Configuração Avançada de Subprocessos
Variáveis de Ambiente e Diretório de Trabalho
Frequentemente é necessário passar variáveis de ambiente ou executar em um diretório específico. Command fornece métodos para ambos:
use std::process::Command;
fn main() {
let output = Command::new("bash")
.arg("-c")
.arg("echo $MY_VAR")
.env("MY_VAR", "Valor_Customizado")
.current_dir("/tmp")
.output()
.expect("Falha ao executar");
println!("{}", String::from_utf8_lossy(&output.stdout));
}
O método env() define variáveis individuais, enquanto envs() aceita um iterador. Para limpar todas as variáveis existentes e usar apenas as configuradas, use env_clear() antes de adicionar as suas.
Tratamento de Stdin e Redirecionamento
Alguns processos requerem entrada. Use Stdio::piped() para stdin também:
use std::process::{Command, Stdio};
use std::io::Write;
fn main() {
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Falha ao iniciar cat");
{
let stdin = child.stdin.as_mut().expect("Falha ao abrir stdin");
stdin.write_all(b"Dados para o cat\n")
.expect("Falha ao escrever");
} // stdin é dropado aqui, sinalizando EOF
let output = child.wait_with_output()
.expect("Falha ao aguardar");
println!("{}", String::from_utf8_lossy(&output.stdout));
}
O bloco {} explícito garante que stdin é dropeado antes de chamar wait_with_output(), sinalizando fim de entrada ao processo filho.
Padrões Importantes e Boas Práticas
Tratamento de Erros e Códigos de Saída
Nem todo processo bem-sucedido retorna código zero. Verifique explicitamente o status:
use std::process::Command;
fn main() {
let status = Command::new("grep")
.arg("inexistente")
.arg("arquivo.txt")
.status()
.expect("Falha ao executar grep");
match status.code() {
Some(0) => println!("Encontrado"),
Some(1) => println!("Não encontrado"),
Some(code) => println!("Erro: código {}", code),
None => println!("Processo terminado por sinal"),
}
}
O método status() é mais leve que output() quando você só precisa do código de saída. code() retorna Option<i32> — None indica morte por sinal em sistemas Unix.
Processos em Paralelo
Para executar múltiplos processos concorrentemente, mantenha referências a seus Child:
use std::process::Command;
fn main() {
let mut children = vec![];
for i in 0..3 {
let child = Command::new("sleep")
.arg("1")
.spawn()
.expect("Falha ao iniciar");
children.push(child);
}
for mut child in children {
child.wait().expect("Falha ao aguardar");
}
println!("Todos os processos terminaram");
}
Esse padrão permite que múltiplos processos executem simultaneamente. Para controle mais sofisticado, considere usar crates como tokio para processamento assíncrono.
Conclusão
Dominando std::process, você consegue integrar qualquer ferramenta externa de forma segura e eficiente. Os três pontos-chave aprendidos foram: (1) Command e spawn() são suas ferramentas principais — escolha output() para resultados pequenos e spawn() com streams para processos longos; (2) sempre trate variáveis de ambiente, diretórios e redirecionamentos explicitamente — Rust exige clareza, evitando bugs sutis; (3) verificar códigos de saída e gerenciar ciclos de vida é responsabilidade sua — o compilador não pode adivinhar a semântica do seu domínio.