stdin, stdout e stderr em Rust
Todo processo possui três fluxos de I/O padrão: stdin (entrada padrão), stdout (saída padrão) e stderr (saída de erro). No Rust, eles são acessados via io::stdin(), io::stdout() e io::stderr(), e usados indiretamente pelas macros print!, println!, eprint! e eprintln!. Entender como funcionam — especialmente o buffering e o locking — é essencial para programas de linha de comando eficientes.
Visão geral e tipos-chave
Os três fluxos
| Fluxo | Função | Tipo retornado | Buffering | Uso |
|---|---|---|---|---|
| stdin | io::stdin() | Stdin | Line-buffered | Ler entrada do usuário ou pipe |
| stdout | io::stdout() | Stdout | Line-buffered (terminal) / Full (pipe) | Saída principal do programa |
| stderr | io::stderr() | Stderr | Sem buffer (imediato) | Mensagens de erro e diagnóstico |
Locking
Os handles Stdin, Stdout e Stderr usam um mutex interno para segurança entre threads. Cada chamada a read_line(), write() etc. adquire e libera esse lock. Para operações intensivas, use lock() para adquirir o lock uma única vez:
// Lento: lock adquirido e liberado a cada println!
for i in 0..1000 {
println!("{}", i);
}
// Rápido: lock adquirido uma vez
let stdout = std::io::stdout();
let mut handle = stdout.lock();
for i in 0..1000 {
writeln!(handle, "{}", i).unwrap();
}
Padrões comuns com código
Ler entrada do usuário
use std::io::{self, Write};
fn main() -> io::Result<()> {
// Padrão básico: prompt + read_line
print!("Digite seu nome: ");
io::stdout().flush()?; // flush necessário após print! (sem newline)
let mut nome = String::new();
io::stdin().read_line(&mut nome)?;
let nome = nome.trim(); // remover \n final
println!("Olá, {}!", nome);
// Ler número
print!("Digite sua idade: ");
io::stdout().flush()?;
let mut entrada = String::new();
io::stdin().read_line(&mut entrada)?;
let idade: u32 = entrada.trim().parse().map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, format!("Idade inválida: {}", e))
})?;
println!("{} tem {} anos", nome, idade);
Ok(())
}
Loop de leitura interativa
use std::io::{self, BufRead, Write};
fn main() -> io::Result<()> {
let stdin = io::stdin();
let mut stdout = io::stdout();
println!("Calculadora simples (digite 'sair' para encerrar)");
println!("Formato: <numero> <operador> <numero>");
let mut linha = String::new();
loop {
print!("> ");
stdout.flush()?;
linha.clear();
let bytes = stdin.read_line(&mut linha)?;
if bytes == 0 {
println!("\nEOF — encerrando");
break;
}
let entrada = linha.trim();
if entrada == "sair" || entrada == "quit" {
println!("Até logo!");
break;
}
let partes: Vec<&str> = entrada.split_whitespace().collect();
if partes.len() != 3 {
eprintln!("Erro: formato esperado: <num> <op> <num>");
continue;
}
let a: f64 = match partes[0].parse() {
Ok(v) => v,
Err(_) => { eprintln!("Erro: '{}' não é um número", partes[0]); continue; }
};
let b: f64 = match partes[2].parse() {
Ok(v) => v,
Err(_) => { eprintln!("Erro: '{}' não é um número", partes[2]); continue; }
};
let resultado = match partes[1] {
"+" => Some(a + b),
"-" => Some(a - b),
"*" => Some(a * b),
"/" if b != 0.0 => Some(a / b),
"/" => { eprintln!("Erro: divisão por zero"); None }
op => { eprintln!("Erro: operador '{}' não reconhecido", op); None }
};
if let Some(r) = resultado {
println!("= {}", r);
}
}
Ok(())
}
Saída formatada com macros
fn main() {
let nome = "Rust";
let versao = 1.76;
// print! e println! — para stdout
println!("Linguagem: {}", nome);
println!("Versão: {:.1}", versao);
println!("{:<20} {:>10}", "Produto", "Preço");
println!("{:<20} {:>10.2}", "Notebook", 4599.90);
println!("{:<20} {:>10.2}", "Mouse", 89.50);
// eprint! e eprintln! — para stderr
eprintln!("[AVISO] Esta mensagem vai para stderr");
eprintln!("[ERRO] Detalhe: {}", "conexão recusada");
// format! — para String (não imprime nada)
let msg = format!("{} v{:.1}", nome, versao);
println!("Formatado: {}", msg);
// Formatação avançada
println!("{:=^40}", " RELATÓRIO "); // centralizado com =
println!("{:#010X}", 255); // 0x000000FF
println!("{:b}", 42); // binário: 101010
println!("{:e}", 1_500_000.0); // notação científica
println!("{value:.prec$}", value = 3.14159, prec = 3); // 3.142
}
Tabela de funções e macros
Funções de I/O padrão
| Função | Retorno | Descrição |
|---|---|---|
io::stdin() | Stdin | Handle da entrada padrão |
io::stdout() | Stdout | Handle da saída padrão |
io::stderr() | Stderr | Handle da saída de erro |
stdin.lock() | StdinLock | Lock exclusivo para stdin |
stdout.lock() | StdoutLock | Lock exclusivo para stdout |
stderr.lock() | StderrLock | Lock exclusivo para stderr |
stdin.read_line(&mut s) | io::Result<usize> | Lê uma linha em String |
stdin.lines() | Lines<StdinLock> | Iterador sobre linhas |
Macros de saída
| Macro | Destino | Descrição |
|---|---|---|
print!(fmt, ...) | stdout | Imprime sem newline |
println!(fmt, ...) | stdout | Imprime com newline |
eprint!(fmt, ...) | stderr | Imprime sem newline em stderr |
eprintln!(fmt, ...) | stderr | Imprime com newline em stderr |
write!(w, fmt, ...) | qualquer Write | Escrita formatada em writer |
writeln!(w, fmt, ...) | qualquer Write | Escrita formatada com newline |
format!(fmt, ...) | String | Formata e retorna String |
Exemplos práticos
Exemplo 1: Processar input via pipe (stdin como fonte de dados)
use std::io::{self, BufRead};
/// Programa que funciona como filtro Unix:
/// cat dados.txt | meu_programa --filtro "ERROR"
fn main() -> io::Result<()> {
let args: Vec<String> = std::env::args().collect();
let filtro = if args.len() > 2 && args[1] == "--filtro" {
Some(args[2].as_str())
} else {
None
};
let stdin = io::stdin();
let mut total = 0u64;
let mut correspondencias = 0u64;
for linha in stdin.lock().lines() {
let linha = linha?;
total += 1;
match filtro {
Some(f) if linha.contains(f) => {
println!("{}", linha);
correspondencias += 1;
}
None => {
println!("{}", linha);
correspondencias += 1;
}
_ => {}
}
}
// Estatísticas vão para stderr (não interferem no pipe)
eprintln!(
"--- Processadas {} linhas, {} correspondências ---",
total, correspondencias
);
Ok(())
}
Exemplo 2: Saída com lock para alta performance
use std::io::{self, Write};
use std::time::Instant;
fn gerar_csv_performatico(num_linhas: usize) -> io::Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock(); // lock uma vez
writeln!(handle, "id,nome,valor,categoria")?;
for i in 0..num_linhas {
writeln!(
handle,
"{},item_{:06},{:.2},cat_{}",
i,
i,
i as f64 * 1.5,
i % 10
)?;
}
handle.flush()?;
Ok(())
}
fn main() -> io::Result<()> {
let inicio = Instant::now();
gerar_csv_performatico(100_000)?;
eprintln!("Gerado em {:?}", inicio.elapsed());
Ok(())
}
Exemplo 3: Separar stdout e stderr corretamente
use std::fs;
use std::io::{self, Write};
fn processar_arquivos(caminhos: &[&str]) -> io::Result<()> {
let mut stdout = io::stdout().lock();
let mut stderr = io::stderr().lock();
let mut sucesso = 0;
let mut falhas = 0;
for caminho in caminhos {
match fs::read_to_string(caminho) {
Ok(conteudo) => {
// Dados vão para stdout (podem ser redirecionados)
writeln!(stdout, "=== {} ({} bytes) ===", caminho, conteudo.len())?;
writeln!(stdout, "{}", conteudo)?;
sucesso += 1;
}
Err(e) => {
// Erros vão para stderr (sempre visíveis no terminal)
writeln!(stderr, "[ERRO] {}: {}", caminho, e)?;
falhas += 1;
}
}
}
// Resumo em stderr
writeln!(stderr, "--- {} ok, {} falhas ---", sucesso, falhas)?;
Ok(())
}
fn main() -> io::Result<()> {
// Uso: ./programa > saida.txt 2> erros.txt
processar_arquivos(&["config.toml", "dados.csv", "inexistente.txt"])?;
Ok(())
}
Exemplo 4: Menu interativo com limpeza de tela
use std::io::{self, Write};
fn limpar_tela() {
// Sequências ANSI para limpar tela
print!("\x1B[2J\x1B[1;1H");
io::stdout().flush().unwrap();
}
fn menu_principal() -> io::Result<String> {
println!("=== Gerenciador de Tarefas ===");
println!();
println!("1. Listar tarefas");
println!("2. Adicionar tarefa");
println!("3. Remover tarefa");
println!("4. Sair");
println!();
print!("Escolha uma opção: ");
io::stdout().flush()?;
let mut escolha = String::new();
io::stdin().read_line(&mut escolha)?;
Ok(escolha.trim().to_string())
}
fn ler_texto(prompt: &str) -> io::Result<String> {
print!("{}", prompt);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn main() -> io::Result<()> {
let mut tarefas: Vec<String> = Vec::new();
loop {
match menu_principal()?.as_str() {
"1" => {
if tarefas.is_empty() {
println!("\nNenhuma tarefa cadastrada.");
} else {
println!("\nTarefas:");
for (i, t) in tarefas.iter().enumerate() {
println!(" {}. {}", i + 1, t);
}
}
}
"2" => {
let tarefa = ler_texto("Nova tarefa: ")?;
if !tarefa.is_empty() {
tarefas.push(tarefa);
println!("Tarefa adicionada!");
}
}
"3" => {
let indice = ler_texto("Número da tarefa para remover: ")?;
if let Ok(i) = indice.parse::<usize>() {
if i > 0 && i <= tarefas.len() {
let removida = tarefas.remove(i - 1);
println!("Removida: {}", removida);
} else {
eprintln!("Índice inválido");
}
}
}
"4" | "sair" => {
println!("Até logo!");
break;
}
_ => eprintln!("Opção inválida"),
}
println!();
}
Ok(())
}
Exemplo 5: Barra de progresso em stderr
use std::io::{self, Write};
use std::thread;
use std::time::Duration;
fn barra_progresso(atual: usize, total: usize, largura: usize) {
let porcentagem = (atual as f64 / total as f64 * 100.0) as usize;
let preenchido = (atual * largura) / total;
let vazio = largura - preenchido;
// \r volta ao início da linha (sem newline)
eprint!(
"\r[{}{}] {:3}% ({}/{})",
"#".repeat(preenchido),
"-".repeat(vazio),
porcentagem,
atual,
total
);
io::stderr().flush().unwrap();
}
fn processar_com_progresso(itens: &[String]) -> io::Result<Vec<String>> {
let total = itens.len();
let mut resultados = Vec::with_capacity(total);
for (i, item) in itens.iter().enumerate() {
// Simular processamento
thread::sleep(Duration::from_millis(50));
resultados.push(item.to_uppercase());
barra_progresso(i + 1, total, 30);
}
eprintln!(); // newline final após a barra
Ok(resultados)
}
fn main() -> io::Result<()> {
let itens: Vec<String> = (0..50).map(|i| format!("item_{}", i)).collect();
eprintln!("Processando {} itens...", itens.len());
let resultados = processar_com_progresso(&itens)?;
// Resultados vão para stdout (podem ser pipados)
for r in &resultados {
println!("{}", r);
}
Ok(())
}
Padrões de tratamento de erro para I/O
Flush obrigatório após print!
A macro print! (sem newline) pode ficar no buffer. Sempre faça flush quando precisar que o texto apareça imediatamente:
use std::io::{self, Write};
fn prompt(mensagem: &str) -> io::Result<String> {
print!("{}", mensagem);
io::stdout().flush()?; // ESSENCIAL após print! sem newline
let mut resposta = String::new();
io::stdin().read_line(&mut resposta)?;
Ok(resposta.trim().to_string())
}
stdin retornando 0 bytes (EOF)
use std::io;
fn ler_obrigatorio(prompt: &str) -> io::Result<String> {
loop {
let mut entrada = String::new();
print!("{}", prompt);
io::stdout().flush()?;
let bytes = io::stdin().read_line(&mut entrada)?;
if bytes == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Entrada encerrada (EOF) antes de receber resposta",
));
}
let texto = entrada.trim().to_string();
if !texto.is_empty() {
return Ok(texto);
}
eprintln!("Entrada não pode ser vazia. Tente novamente.");
}
}
Dicas de desempenho
- Use
lock()em loops — evita adquirir/liberar o mutex a cada operação. A diferença pode ser de 10x ou mais. - stderr não é bufferizado — cada
eprintln!é uma syscall. Para logging intensivo, considere escrever em arquivo comBufWriter. - stdout é line-buffered no terminal mas full-buffered em pipe —
flush()é necessário se quiser saída imediata em pipe. - Reutilize o buffer de
read_line()chamandoclear()em vez de criar uma novaStringa cada iteração.
Veja também
- Módulo std::io — visão geral do sistema de I/O
- Read e Write Traits — traits implementadas por Stdin/Stdout
- BufReader e BufWriter — buffering para performance
- Módulo std::process — piping entre processos
- Ler Input do Usuário — receita prática de leitura interativa
- Documentação oficial:
std::io::stdin,std::io::stdout