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:
| Ferramenta | Substitui | Destaque |
|---|---|---|
| ripgrep (rg) | grep | 5-10x mais rápido que grep, respeita .gitignore |
| bat | cat | Syntax highlighting, integração com Git |
| eza | ls | Ícones, cores, tree view integrada |
| fd | find | Sintaxe simples, 5x mais rápido, respeita .gitignore |
| dust | du | Visualização interativa de uso de disco |
| delta | diff | Syntax highlighting para diffs do Git |
| zoxide | cd | Navegação inteligente com aprendizado de hábitos |
| starship | prompt | Prompt customizável, suporte a 40+ linguagens |
| bottom (btm) | top/htop | Monitor de sistema gráfico no terminal |
| hyperfine | time | Benchmarking 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
- Fundamentos: Aprenda Rust com nosso tutorial de primeiros passos
- Tratamento de erros: Essencial para CLIs — veja o tutorial de tratamento de erros
- Primeira CLI: Siga o tutorial de CLI com Clap
- Leitura de arquivos: Aprenda a ler arquivos e parsear TOML
- Argumentos: Domine a receita de argumentos de linha de comando
- 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
- Tutorial: CLI com Clap — Crie sua primeira CLI em Rust passo a passo
- Receita: Ler Argumentos CLI — Como processar argumentos de linha de comando
- Receita: Ler Input do Usuário — Leitura interativa no terminal
- Rust para DevOps — Ferramentas de infraestrutura em Rust
- Rust para Web — Se sua CLI precisa de uma interface web
- Instalação do Rust — Configure seu ambiente de desenvolvimento
- Empresas que Usam Rust — Veja quem está contratando