Rust CLI: Crie Ferramentas de Linha de Comando | Rust Brasil

Crie CLIs profissionais em Rust com Clap e dialoguer. Saída colorida, distribuição multiplataforma e exemplos práticos.

Introdução

Ferramentas de linha de comando são uma das áreas onde Rust mais brilha. As razões são claras: binários estáticos que não dependem de runtime ou interpretador, startup instantâneo (sem JIT ou carregamento de VM), baixo consumo de memória e a capacidade de distribuir um único executável para qualquer plataforma. Não é coincidência que algumas das ferramentas de terminal mais populares da última década foram escritas em Rust: ripgrep, bat, eza, fd, dust, delta, zoxide e starship.

Se você é um desenvolvedor que cria scripts em Python ou Bash e quer transformá-los em ferramentas robustas e rápidas, Rust é a escolha ideal. Neste artigo, vamos explorar o ecossistema de bibliotecas para CLIs, construir uma ferramenta completa e entender como distribuir seus binários para o mundo.

Ecossistema de Bibliotecas para CLI

Clap — Parser de Argumentos

Clap é a biblioteca padrão para parsing de argumentos de linha de comando. Suporta subcomandos, validação, autocompletion e geração automática de help:

[package]
name = "minha-cli"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
indicatif = "0.17"
dialoguer = "0.11"
colored = "2"
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Indicatif — Barras de Progresso

Indicatif cria barras de progresso e spinners elegantes para operações demoradas.

Dialoguer — Prompts Interativos

Dialoguer oferece prompts de confirmação, seleção, input de texto e senhas para CLIs interativas.

Colored — Saída Colorida

Colored permite adicionar cores e estilos ao output do terminal de forma simples e expressiva.

Exemplo Prático: Gerenciador de Projetos CLI

Vamos construir uma ferramenta completa de gerenciamento de projetos com subcomandos, barras de progresso e saída colorida.

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use colored::*;
use dialoguer::{Confirm, Input, Select};
use indicatif::{ProgressBar, ProgressStyle};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::time::Duration;

#[derive(Parser)]
#[command(name = "projetos")]
#[command(about = "Gerenciador de projetos no terminal", version)]
struct Cli {
    #[command(subcommand)]
    comando: Comandos,

    /// Caminho do arquivo de dados
    #[arg(short, long, default_value = "projetos.json")]
    arquivo: PathBuf,
}

#[derive(Subcommand)]
enum Comandos {
    /// Criar um novo projeto
    Novo {
        /// Nome do projeto
        nome: Option<String>,
    },
    /// Listar todos os projetos
    Listar {
        /// Filtrar por status
        #[arg(short, long)]
        status: Option<String>,
    },
    /// Marcar projeto como concluído
    Concluir {
        /// ID do projeto
        id: usize,
    },
    /// Remover um projeto
    Remover {
        /// ID do projeto
        id: usize,
    },
    /// Exportar relatório
    Relatorio,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Projeto {
    id: usize,
    nome: String,
    descricao: String,
    status: String,
    prioridade: String,
}

#[derive(Debug, Serialize, Deserialize, Default)]
struct Dados {
    projetos: Vec<Projeto>,
    proximo_id: usize,
}

impl Dados {
    fn carregar(caminho: &PathBuf) -> Result<Self> {
        if caminho.exists() {
            let conteudo = fs::read_to_string(caminho)
                .context("Falha ao ler arquivo de dados")?;
            serde_json::from_str(&conteudo)
                .context("Falha ao parsear dados")
        } else {
            Ok(Dados {
                projetos: Vec::new(),
                proximo_id: 1,
            })
        }
    }

    fn salvar(&self, caminho: &PathBuf) -> Result<()> {
        let conteudo = serde_json::to_string_pretty(self)?;
        fs::write(caminho, conteudo)
            .context("Falha ao salvar dados")?;
        Ok(())
    }
}

fn criar_projeto(dados: &mut Dados, nome: Option<String>) -> Result<()> {
    let nome = match nome {
        Some(n) => n,
        None => Input::new()
            .with_prompt("Nome do projeto")
            .interact_text()?,
    };

    let descricao: String = Input::new()
        .with_prompt("Descrição")
        .interact_text()?;

    let prioridades = vec!["Alta", "Média", "Baixa"];
    let selecao = Select::new()
        .with_prompt("Prioridade")
        .items(&prioridades)
        .default(1)
        .interact()?;

    let projeto = Projeto {
        id: dados.proximo_id,
        nome: nome.clone(),
        descricao,
        status: "Em andamento".to_string(),
        prioridade: prioridades[selecao].to_string(),
    };

    dados.projetos.push(projeto);
    dados.proximo_id += 1;

    println!(
        "{} Projeto '{}' criado com ID {}",
        "✓".green().bold(),
        nome.cyan(),
        (dados.proximo_id - 1).to_string().yellow()
    );

    Ok(())
}

fn listar_projetos(dados: &Dados, status: Option<String>) {
    let projetos: Vec<&Projeto> = match &status {
        Some(s) => dados.projetos.iter()
            .filter(|p| p.status.to_lowercase().contains(&s.to_lowercase()))
            .collect(),
        None => dados.projetos.iter().collect(),
    };

    if projetos.is_empty() {
        println!("{}", "Nenhum projeto encontrado.".yellow());
        return;
    }

    println!("\n{}", "═══ Projetos ═══".bold().cyan());
    for p in projetos {
        let status_cor = match p.status.as_str() {
            "Concluído" => p.status.green(),
            "Em andamento" => p.status.yellow(),
            _ => p.status.white(),
        };
        let prioridade_cor = match p.prioridade.as_str() {
            "Alta" => p.prioridade.red(),
            "Média" => p.prioridade.yellow(),
            _ => p.prioridade.green(),
        };

        println!(
            "  [{}] {}{} [{}] [{}]",
            p.id.to_string().bold(),
            p.nome.white().bold(),
            p.descricao.dimmed(),
            status_cor,
            prioridade_cor
        );
    }
    println!();
}

fn concluir_projeto(dados: &mut Dados, id: usize) -> Result<()> {
    let projeto = dados.projetos.iter_mut()
        .find(|p| p.id == id)
        .context(format!("Projeto com ID {} não encontrado", id))?;

    // Simulação de progresso
    let pb = ProgressBar::new(100);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}% {msg}")?
            .progress_chars("█▓░"),
    );
    pb.set_message("Finalizando projeto...");

    for i in 0..100 {
        pb.set_position(i + 1);
        std::thread::sleep(Duration::from_millis(15));
    }
    pb.finish_with_message("Concluído!");

    projeto.status = "Concluído".to_string();
    println!(
        "\n{} Projeto '{}' marcado como concluído!",
        "✓".green().bold(),
        projeto.nome.cyan()
    );

    Ok(())
}

fn remover_projeto(dados: &mut Dados, id: usize) -> Result<()> {
    let nome = dados.projetos.iter()
        .find(|p| p.id == id)
        .map(|p| p.nome.clone())
        .context(format!("Projeto com ID {} não encontrado", id))?;

    let confirmar = Confirm::new()
        .with_prompt(format!("Remover projeto '{}'?", nome))
        .default(false)
        .interact()?;

    if confirmar {
        dados.projetos.retain(|p| p.id != id);
        println!("{} Projeto removido.", "✓".green().bold());
    } else {
        println!("{}", "Operação cancelada.".yellow());
    }

    Ok(())
}

fn gerar_relatorio(dados: &Dados) {
    let total = dados.projetos.len();
    let concluidos = dados.projetos.iter()
        .filter(|p| p.status == "Concluído")
        .count();
    let em_andamento = total - concluidos;

    println!("\n{}", "═══ Relatório ═══".bold().cyan());
    println!("  Total de projetos:  {}", total.to_string().bold());
    println!("  Em andamento:       {}", em_andamento.to_string().yellow());
    println!("  Concluídos:         {}", concluidos.to_string().green());

    if total > 0 {
        let porcentagem = (concluidos as f64 / total as f64) * 100.0;
        let pb = ProgressBar::new(100);
        pb.set_style(
            ProgressStyle::default_bar()
                .template("  Progresso: [{bar:30.green/white}] {pos}%")?
                .progress_chars("█▓░"),
        );
        // unwrap seguro: template é válida (definida acima)
        pb.set_position(porcentagem as u64);
        pb.abandon(); // manter a barra visível
    }
    println!();
}

fn main() -> Result<()> {
    let cli = Cli::parse();
    let mut dados = Dados::carregar(&cli.arquivo)?;

    match cli.comando {
        Comandos::Novo { nome } => criar_projeto(&mut dados, nome)?,
        Comandos::Listar { status } => listar_projetos(&dados, status),
        Comandos::Concluir { id } => concluir_projeto(&mut dados, id)?,
        Comandos::Remover { id } => remover_projeto(&mut dados, id)?,
        Comandos::Relatorio => gerar_relatorio(&dados),
    }

    dados.salvar(&cli.arquivo)?;
    Ok(())
}

Uso da Ferramenta

# Criar um novo projeto
projetos novo "Reescrever API em Rust"

# Listar todos os projetos
projetos listar

# Filtrar por status
projetos listar --status concluído

# Marcar como concluído
projetos concluir 1

# Gerar relatório
projetos relatorio

CLIs Famosas Escritas em Rust

Rust é responsável por uma nova geração de ferramentas de terminal que substituem utilitários clássicos do Unix com versões mais rápidas e amigáveis:

FerramentaSubstituiDestaque
ripgrep (rg)grep5-10x mais rápido que grep, respeita .gitignore
batcatSyntax highlighting, integração com Git
ezalsÍcones, cores, tree view integrada
fdfindSintaxe simples, 5x mais rápido, respeita .gitignore
dustduVisualização interativa de uso de disco
deltadiffSyntax highlighting para diffs do Git
zoxidecdNavegação inteligente com aprendizado de hábitos
starshippromptPrompt customizável, suporte a 40+ linguagens
bottom (btm)top/htopMonitor de sistema gráfico no terminal
hyperfinetimeBenchmarking de comandos com estatísticas

Essas ferramentas demonstram por que Rust é ideal para CLIs: binários pequenos (geralmente 2-10MB), startup em milissegundos, sem dependência de runtime e fácil distribuição.

Distribuição de Binários

Cross-compilation

Com cross ou cargo-zigbuild, você pode compilar para múltiplas plataformas:

# Instalar cross
cargo install cross

# Compilar para Linux
cross build --release --target x86_64-unknown-linux-musl

# Compilar para macOS
cross build --release --target x86_64-apple-darwin

# Compilar para Windows
cross build --release --target x86_64-pc-windows-gnu

GitHub Actions para Release Automático

name: Release
on:
  push:
    tags: ["v*"]

jobs:
  build:
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - run: cargo build --release --target ${{ matrix.target }}
      - uses: softprops/action-gh-release@v2
        with:
          files: target/${{ matrix.target }}/release/minha-cli*

Publicação no crates.io

# Login
cargo login

# Publicar
cargo publish

Após publicar, qualquer pessoa com Rust instalado pode instalar com cargo install minha-cli.

Empresas Usando Rust para CLIs

  • GitHub: CLI interna de ferramentas de infraestrutura
  • Mozilla: Ferramentas de build e empacotamento
  • Vercel: Turbopack e ferramentas de build (turborepo) contêm componentes Rust
  • Amazon (AWS): Ferramentas internas de deploy e monitoramento
  • Astral: uv (gerenciador de pacotes Python ultrarrápido) e ruff (linter Python) são escritos em Rust

Como Começar

  1. Fundamentos: Aprenda Rust com nosso tutorial de primeiros passos
  2. Tratamento de erros: Essencial para CLIs — veja o tutorial de tratamento de erros
  3. Primeira CLI: Siga o tutorial de CLI com Clap
  4. Leitura de arquivos: Aprenda a ler arquivos e parsear TOML
  5. Argumentos: Domine a receita de argumentos de linha de comando
  6. Distribua: Configure GitHub Actions para releases automáticos

Conclusão

Rust é, sem dúvida, a melhor linguagem para ferramentas de linha de comando em 2026. A combinação de binários estáticos sem dependências, performance de linguagem compilada, ecossistema maduro de bibliotecas (Clap, indicatif, dialoguer) e facilidade de distribuição cria uma experiência incomparável tanto para desenvolvedores quanto para usuários finais. Se você ainda está escrevendo scripts em Python ou Bash, considere reescrevê-los em Rust — seus usuários vão agradecer.


Veja Também