stdin, stdout e stderr em Rust

Referência completa de stdin, stdout e stderr em Rust: io::stdin, io::stdout, lock, read_line, print!, println!, eprint!, eprintln! e piping.

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

FluxoFunçãoTipo retornadoBufferingUso
stdinio::stdin()StdinLine-bufferedLer entrada do usuário ou pipe
stdoutio::stdout()StdoutLine-buffered (terminal) / Full (pipe)Saída principal do programa
stderrio::stderr()StderrSem 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çãoRetornoDescrição
io::stdin()StdinHandle da entrada padrão
io::stdout()StdoutHandle da saída padrão
io::stderr()StderrHandle da saída de erro
stdin.lock()StdinLockLock exclusivo para stdin
stdout.lock()StdoutLockLock exclusivo para stdout
stderr.lock()StderrLockLock 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

MacroDestinoDescrição
print!(fmt, ...)stdoutImprime sem newline
println!(fmt, ...)stdoutImprime com newline
eprint!(fmt, ...)stderrImprime sem newline em stderr
eprintln!(fmt, ...)stderrImprime com newline em stderr
write!(w, fmt, ...)qualquer WriteEscrita formatada em writer
writeln!(w, fmt, ...)qualquer WriteEscrita formatada com newline
format!(fmt, ...)StringFormata 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 com BufWriter.
  • stdout é line-buffered no terminal mas full-buffered em pipeflush() é necessário se quiser saída imediata em pipe.
  • Reutilize o buffer de read_line() chamando clear() em vez de criar uma nova String a cada iteração.

Veja também