Introdução
O Clap (Command Line Argument Parser) é a biblioteca mais popular para criar interfaces de linha de comando em Rust. Com sua API Derive, você define argumentos e subcomandos usando atributos em structs e enums, e o Clap gera automaticamente o parsing, validação, mensagens de help e autocompletion.
Se você precisa construir qualquer ferramenta CLI – desde scripts simples com poucos flags até aplicações complexas com múltiplos subcomandos – o Clap é a escolha padrão do ecossistema Rust.
Por que usar o Clap?
- API Derive ergonômica: defina CLI com structs e atributos
- Validação automática: tipos, ranges, valores permitidos
- Help gerado: mensagens de ajuda formatadas automaticamente
- Subcomandos: hierarquia de comandos como
git,cargo - Shell completion: gere scripts de autocompletion para bash, zsh, fish
- Colorido: output com cores no terminal
- Testável: teste sua CLI programaticamente
- Ecossistema: integração com tracing, config, indicatif e outras crates
Instalação
Adicione o Clap ao seu Cargo.toml:
[dependencies]
clap = { version = "4", features = ["derive"] }
Features opcionais:
[dependencies]
clap = { version = "4", features = [
"derive", # API com derive macros (recomendado)
"env", # Ler valores de variáveis de ambiente
"unicode", # Suporte a Unicode
"wrap_help", # Quebra de linha automática no help
"color", # Cores no output (habilitado por padrão)
] }
# Para gerar shell completions
clap_complete = "4"
Uso Básico
CLI simples com argumentos e flags
use clap::Parser;
/// Ferramenta para cumprimentar pessoas
#[derive(Parser, Debug)]
#[command(name = "saudacao")]
#[command(version = "1.0")]
#[command(about = "Cumprimente pessoas no terminal")]
struct Cli {
/// Nome da pessoa para cumprimentar
nome: String,
/// Número de vezes para repetir a saudação
#[arg(short, long, default_value_t = 1)]
vezes: u32,
/// Usar saudação formal
#[arg(short, long)]
formal: bool,
/// Idioma da saudação
#[arg(short, long, default_value = "pt")]
idioma: String,
}
fn main() {
let cli = Cli::parse();
let saudacao = if cli.formal {
match cli.idioma.as_str() {
"pt" => format!("Prezado(a) {}, bom dia!", cli.nome),
"en" => format!("Dear {}, good morning!", cli.nome),
"es" => format!("Estimado(a) {}, buenos días!", cli.nome),
_ => format!("Olá, {}!", cli.nome),
}
} else {
match cli.idioma.as_str() {
"pt" => format!("E aí, {}!", cli.nome),
"en" => format!("Hey, {}!", cli.nome),
"es" => format!("Hola, {}!", cli.nome),
_ => format!("Olá, {}!", cli.nome),
}
};
for _ in 0..cli.vezes {
println!("{}", saudacao);
}
}
// Uso:
// $ saudacao Maria
// E aí, Maria!
//
// $ saudacao "João Silva" --formal --vezes 3
// Prezado(a) João Silva, bom dia!
// Prezado(a) João Silva, bom dia!
// Prezado(a) João Silva, bom dia!
//
// $ saudacao --help
Argumentos opcionais e com valores padrão
use clap::Parser;
use std::path::PathBuf;
/// Processador de arquivos de texto
#[derive(Parser, Debug)]
#[command(version, about)]
struct Cli {
/// Arquivo de entrada
#[arg(short, long)]
entrada: PathBuf,
/// Arquivo de saída (padrão: stdout)
#[arg(short = 'o', long)]
saida: Option<PathBuf>,
/// Número máximo de linhas para processar
#[arg(short = 'n', long)]
max_linhas: Option<usize>,
/// Codificação do arquivo
#[arg(long, default_value = "utf-8")]
encoding: String,
/// Modo verbose
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
/// Modo silencioso (sem output)
#[arg(short = 'q', long)]
quiet: bool,
}
fn main() {
let cli = Cli::parse();
match cli.verbose {
0 => {} // Normal
1 => println!("Modo verbose ativado"),
2 => println!("Modo muito verbose ativado"),
_ => println!("Modo debug completo"),
}
println!("Entrada: {}", cli.entrada.display());
if let Some(ref saida) = cli.saida {
println!("Saída: {}", saida.display());
} else {
println!("Saída: stdout");
}
if let Some(max) = cli.max_linhas {
println!("Máximo de linhas: {}", max);
}
}
// Uso:
// $ processador -e arquivo.txt
// $ processador -e arquivo.txt -o resultado.txt -n 100 -vv
Subcomandos
use clap::{Parser, Subcommand};
/// Gerenciador de tarefas
#[derive(Parser, Debug)]
#[command(name = "tarefas")]
#[command(version, about = "Gerencie suas tarefas pelo terminal")]
struct Cli {
#[command(subcommand)]
comando: Comandos,
}
#[derive(Subcommand, Debug)]
enum Comandos {
/// Adicionar uma nova tarefa
Adicionar {
/// Título da tarefa
titulo: String,
/// Prioridade (1-5)
#[arg(short, long, default_value_t = 3, value_parser = clap::value_parser!(u8).range(1..=5))]
prioridade: u8,
/// Tags da tarefa
#[arg(short, long, num_args = 1..)]
tags: Vec<String>,
},
/// Listar todas as tarefas
Listar {
/// Filtrar por status
#[arg(short, long)]
status: Option<String>,
/// Mostrar apenas as N primeiras
#[arg(short = 'n', long)]
limite: Option<usize>,
},
/// Concluir uma tarefa
Concluir {
/// ID da tarefa
id: u64,
},
/// Remover uma tarefa
Remover {
/// ID da tarefa
id: u64,
/// Não pedir confirmação
#[arg(short = 'y', long)]
sim: bool,
},
/// Buscar tarefas por texto
Buscar {
/// Termo de busca
termo: String,
},
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comandos::Adicionar { titulo, prioridade, tags } => {
println!("Adicionando tarefa: '{}'", titulo);
println!(" Prioridade: {}", prioridade);
if !tags.is_empty() {
println!(" Tags: {}", tags.join(", "));
}
}
Comandos::Listar { status, limite } => {
println!("Listando tarefas...");
if let Some(s) = status {
println!(" Filtro: {}", s);
}
if let Some(n) = limite {
println!(" Limite: {}", n);
}
}
Comandos::Concluir { id } => {
println!("Concluindo tarefa #{}", id);
}
Comandos::Remover { id, sim } => {
if sim {
println!("Removendo tarefa #{}", id);
} else {
println!("Tem certeza que deseja remover a tarefa #{}? (use -y)", id);
}
}
Comandos::Buscar { termo } => {
println!("Buscando por: '{}'", termo);
}
}
}
// Uso:
// $ tarefas adicionar "Estudar Rust" -p 5 -t rust estudo
// $ tarefas listar --status pendente
// $ tarefas concluir 42
// $ tarefas remover 42 -y
// $ tarefas buscar "Rust"
Recursos Avançados
Validação de valores
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser, Debug)]
struct Cli {
/// Porta do servidor (1024-65535)
#[arg(short, long, default_value_t = 8080, value_parser = clap::value_parser!(u16).range(1024..=65535))]
porta: u16,
/// Nível de log
#[arg(short, long, default_value = "info", value_parser = ["trace", "debug", "info", "warn", "error"])]
log_level: String,
/// Arquivo de configuração (deve existir)
#[arg(short, long, value_parser = validar_arquivo_existe)]
config: Option<PathBuf>,
/// Email do administrador
#[arg(long, value_parser = validar_email)]
admin_email: Option<String>,
/// Número de workers (1-256)
#[arg(short, long, default_value_t = 4)]
workers: u16,
}
fn validar_arquivo_existe(caminho: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(caminho);
if path.exists() {
Ok(path)
} else {
Err(format!("Arquivo não encontrado: {}", caminho))
}
}
fn validar_email(email: &str) -> Result<String, String> {
if email.contains('@') && email.contains('.') {
Ok(email.to_string())
} else {
Err(format!("Email inválido: {}", email))
}
}
fn main() {
let cli = Cli::parse();
println!("Porta: {}", cli.porta);
println!("Log level: {}", cli.log_level);
println!("Workers: {}", cli.workers);
}
Enums como valores de argumento
use clap::{Parser, ValueEnum};
#[derive(Debug, Clone, ValueEnum)]
enum Formato {
/// Saída em JSON
Json,
/// Saída em YAML
Yaml,
/// Saída em formato tabular
Tabela,
/// Saída em CSV
Csv,
}
#[derive(Debug, Clone, ValueEnum)]
enum Cor {
Vermelho,
Verde,
Azul,
Amarelo,
}
#[derive(Parser, Debug)]
struct Cli {
/// Formato de saída
#[arg(short, long, default_value = "tabela")]
formato: Formato,
/// Cor do destaque
#[arg(long)]
cor: Option<Cor>,
}
fn main() {
let cli = Cli::parse();
match cli.formato {
Formato::Json => println!("Gerando output JSON..."),
Formato::Yaml => println!("Gerando output YAML..."),
Formato::Tabela => println!("Gerando output em tabela..."),
Formato::Csv => println!("Gerando output CSV..."),
}
}
// Uso:
// $ app --formato json
// $ app -f yaml --cor verde
// Valores inválidos mostram os valores permitidos automaticamente
Variáveis de ambiente
use clap::Parser;
#[derive(Parser, Debug)]
#[command(about = "Servidor web configurável")]
struct Cli {
/// Host para bind (ou env: HOST)
#[arg(long, env = "HOST", default_value = "0.0.0.0")]
host: String,
/// Porta do servidor (ou env: PORT)
#[arg(short, long, env = "PORT", default_value_t = 3000)]
porta: u16,
/// URL do banco de dados (ou env: DATABASE_URL)
#[arg(long, env = "DATABASE_URL")]
database_url: String,
/// Chave secreta para JWT (ou env: JWT_SECRET)
#[arg(long, env = "JWT_SECRET")]
jwt_secret: String,
/// Nível de log (ou env: RUST_LOG)
#[arg(long, env = "RUST_LOG", default_value = "info")]
log_level: String,
}
fn main() {
let cli = Cli::parse();
println!("Servidor: {}:{}", cli.host, cli.porta);
println!("Database: {}", cli.database_url);
println!("Log level: {}", cli.log_level);
}
// Uso:
// $ DATABASE_URL=postgres://localhost/db JWT_SECRET=s3cr3t app
// $ app --database-url postgres://localhost/db --jwt-secret s3cr3t --porta 8080
Grupos de argumentos
use clap::{Args, Parser};
#[derive(Parser, Debug)]
struct Cli {
#[command(flatten)]
conexao: ConexaoArgs,
#[command(flatten)]
output: OutputArgs,
/// Modo verbose
#[arg(short, long)]
verbose: bool,
}
#[derive(Args, Debug)]
#[group(required = true, multiple = false)]
struct ConexaoArgs {
/// Conectar via URL
#[arg(long)]
url: Option<String>,
/// Conectar via host e porta
#[arg(long, requires = "porta")]
host: Option<String>,
/// Porta (requer --host)
#[arg(long)]
porta: Option<u16>,
}
#[derive(Args, Debug)]
struct OutputArgs {
/// Formato de saída
#[arg(short, long, default_value = "texto")]
formato: String,
/// Arquivo de saída
#[arg(short = 'o', long)]
saida: Option<String>,
/// Sem cores
#[arg(long)]
sem_cor: bool,
}
fn main() {
let cli = Cli::parse();
println!("{:?}", cli);
}
Shell completion
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use std::io;
#[derive(Parser)]
#[command(name = "minha-cli")]
struct Cli {
#[command(subcommand)]
comando: Comandos,
}
#[derive(Subcommand)]
enum Comandos {
/// Executar tarefa
Run {
#[arg(short, long)]
nome: String,
},
/// Gerar shell completion
Completions {
/// Shell para gerar (bash, zsh, fish, powershell)
#[arg(value_enum)]
shell: Shell,
},
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comandos::Run { nome } => {
println!("Executando: {}", nome);
}
Comandos::Completions { shell } => {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "minha-cli", &mut io::stdout());
}
}
}
// Gerar completions:
// $ minha-cli completions bash > /etc/bash_completion.d/minha-cli
// $ minha-cli completions zsh > ~/.zsh/completions/_minha-cli
// $ minha-cli completions fish > ~/.config/fish/completions/minha-cli.fish
Subcomandos aninhados
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "projeto")]
#[command(about = "Gerenciador de projetos")]
struct Cli {
#[command(subcommand)]
comando: Comandos,
}
#[derive(Subcommand)]
enum Comandos {
/// Gerenciar repositórios
Repo {
#[command(subcommand)]
acao: RepoAcoes,
},
/// Gerenciar deploys
Deploy {
#[command(subcommand)]
acao: DeployAcoes,
},
}
#[derive(Subcommand)]
enum RepoAcoes {
/// Criar novo repositório
Criar {
nome: String,
#[arg(long)]
privado: bool,
},
/// Listar repositórios
Listar {
#[arg(short = 'n', long, default_value_t = 10)]
limite: usize,
},
/// Clonar repositório
Clonar {
url: String,
},
}
#[derive(Subcommand)]
enum DeployAcoes {
/// Criar novo deploy
Criar {
#[arg(short, long)]
ambiente: String,
#[arg(short, long)]
versao: String,
},
/// Status do deploy
Status {
#[arg(short, long)]
ambiente: String,
},
/// Rollback do deploy
Rollback {
#[arg(short, long)]
ambiente: String,
},
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comandos::Repo { acao } => match acao {
RepoAcoes::Criar { nome, privado } => {
println!("Criando repo '{}' (privado: {})", nome, privado);
}
RepoAcoes::Listar { limite } => {
println!("Listando {} repos...", limite);
}
RepoAcoes::Clonar { url } => {
println!("Clonando: {}", url);
}
},
Comandos::Deploy { acao } => match acao {
DeployAcoes::Criar { ambiente, versao } => {
println!("Deploy v{} para {}", versao, ambiente);
}
DeployAcoes::Status { ambiente } => {
println!("Status do deploy em {}", ambiente);
}
DeployAcoes::Rollback { ambiente } => {
println!("Rollback em {}", ambiente);
}
},
}
}
// Uso:
// $ projeto repo criar meu-projeto --privado
// $ projeto repo listar -n 20
// $ projeto deploy criar -a producao -v 2.1.0
// $ projeto deploy status -a producao
Boas Práticas
1. Use a Derive API
use clap::Parser;
// Bom: Derive API - declarativo e conciso
#[derive(Parser)]
struct Cli {
#[arg(short, long)]
nome: String,
}
// A Builder API ainda existe para casos especiais,
// mas a Derive API cobre 99% dos casos de uso
2. Documente cada argumento e comando
use clap::Parser;
/// Ferramenta de deploy para ambientes cloud
///
/// Exemplos:
/// deploy --ambiente staging --versao 1.2.3
/// deploy --ambiente producao --versao latest --dry-run
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// Ambiente alvo (staging, producao)
#[arg(short, long)]
ambiente: String,
/// Versão para deploy (ex: 1.2.3 ou latest)
#[arg(short, long)]
versao: String,
/// Simular sem executar (dry run)
#[arg(long)]
dry_run: bool,
}
3. Teste sua CLI
use clap::Parser;
#[derive(Parser, Debug, PartialEq)]
struct Cli {
#[arg(short, long)]
nome: String,
#[arg(short, long, default_value_t = 8080)]
porta: u16,
#[arg(short, long)]
verbose: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_argumentos_basicos() {
let cli = Cli::parse_from(["app", "--nome", "teste"]);
assert_eq!(cli.nome, "teste");
assert_eq!(cli.porta, 8080);
assert!(!cli.verbose);
}
#[test]
fn test_todos_argumentos() {
let cli = Cli::parse_from([
"app", "--nome", "prod", "--porta", "3000", "--verbose",
]);
assert_eq!(cli.nome, "prod");
assert_eq!(cli.porta, 3000);
assert!(cli.verbose);
}
#[test]
fn test_args_curtos() {
let cli = Cli::parse_from(["app", "-n", "dev", "-p", "9090", "-v"]);
assert_eq!(cli.nome, "dev");
assert_eq!(cli.porta, 9090);
assert!(cli.verbose);
}
#[test]
fn test_argumento_obrigatorio() {
let resultado = Cli::try_parse_from(["app"]);
assert!(resultado.is_err());
}
}
4. Use nomes claros e consistentes
use clap::Parser;
#[derive(Parser)]
struct Cli {
// Bom: nomes descritivos e curtos
#[arg(short, long)]
output: String,
// Bom: short personalizados para evitar conflitos
#[arg(short = 'n', long)]
dry_run: bool,
// Bom: use value_name para help mais claro
#[arg(short, long, value_name = "SEGUNDOS")]
timeout: Option<u64>,
}
5. Forneça valores padrão sensatos
use clap::Parser;
#[derive(Parser)]
struct ServerCli {
#[arg(long, default_value = "0.0.0.0")]
host: String,
#[arg(short, long, default_value_t = 3000)]
porta: u16,
#[arg(long, default_value_t = num_cpus::get())]
workers: usize,
#[arg(long, default_value = "info")]
log_level: String,
}
Exemplos Práticos
Aplicação CLI completa
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
/// Gerenciador de notas pessoais
#[derive(Parser)]
#[command(name = "notas")]
#[command(version = "1.0.0")]
#[command(about = "Gerencie suas notas pelo terminal")]
#[command(propagate_version = true)]
struct Cli {
/// Arquivo do banco de dados
#[arg(long, default_value = "~/.notas.db", global = true)]
db: PathBuf,
/// Formato de saída
#[arg(short, long, default_value = "texto", global = true)]
formato: Formato,
#[command(subcommand)]
comando: Comandos,
}
#[derive(Clone, ValueEnum)]
enum Formato {
Texto,
Json,
Csv,
}
#[derive(Subcommand)]
enum Comandos {
/// Criar uma nova nota
Nova {
/// Título da nota
titulo: String,
/// Corpo da nota (abre editor se omitido)
#[arg(short, long)]
corpo: Option<String>,
/// Tags da nota
#[arg(short, long, num_args = 1..)]
tags: Vec<String>,
/// Marcar como favorita
#[arg(short = 'f', long)]
favorita: bool,
},
/// Listar notas
Listar {
/// Filtrar por tag
#[arg(short, long)]
tag: Option<String>,
/// Mostrar apenas favoritas
#[arg(short, long)]
favoritas: bool,
/// Número máximo de notas
#[arg(short = 'n', long, default_value_t = 20)]
limite: usize,
/// Ordenar por (data, titulo, prioridade)
#[arg(short, long, default_value = "data")]
ordenar: String,
},
/// Ver detalhes de uma nota
Ver {
/// ID da nota
id: u64,
},
/// Editar uma nota existente
Editar {
/// ID da nota
id: u64,
/// Novo título
#[arg(short, long)]
titulo: Option<String>,
/// Novo corpo
#[arg(short, long)]
corpo: Option<String>,
},
/// Remover uma nota
Remover {
/// IDs das notas para remover
#[arg(required = true, num_args = 1..)]
ids: Vec<u64>,
/// Não pedir confirmação
#[arg(short = 'y', long)]
sim: bool,
},
/// Buscar notas por texto
Buscar {
/// Termo de busca
termo: String,
/// Buscar apenas nos títulos
#[arg(long)]
apenas_titulo: bool,
},
/// Exportar notas
Exportar {
/// Arquivo de saída
#[arg(short, long)]
arquivo: PathBuf,
/// Formato de exportação
#[arg(short, long, default_value = "json")]
formato_export: String,
},
/// Importar notas
Importar {
/// Arquivo para importar
arquivo: PathBuf,
},
/// Mostrar estatísticas
Stats,
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comandos::Nova { titulo, corpo, tags, favorita } => {
let corpo = corpo.unwrap_or_else(|| {
// Na prática, abriria o editor aqui
println!("(Abrindo editor para editar o corpo...)");
"Corpo da nota".to_string()
});
println!("Criando nota:");
println!(" Título: {}", titulo);
println!(" Corpo: {}...", &corpo[..corpo.len().min(50)]);
println!(" Tags: {:?}", tags);
println!(" Favorita: {}", favorita);
}
Comandos::Listar { tag, favoritas, limite, ordenar } => {
println!("Listando notas (limite: {}, ordem: {})", limite, ordenar);
if let Some(t) = tag {
println!(" Filtro por tag: {}", t);
}
if favoritas {
println!(" Apenas favoritas");
}
}
Comandos::Ver { id } => {
println!("Visualizando nota #{}", id);
}
Comandos::Editar { id, titulo, corpo } => {
println!("Editando nota #{}", id);
if let Some(t) = titulo {
println!(" Novo título: {}", t);
}
if let Some(c) = corpo {
println!(" Novo corpo: {}...", &c[..c.len().min(50)]);
}
}
Comandos::Remover { ids, sim } => {
if sim || confirmar_remocao(&ids) {
for id in &ids {
println!("Removendo nota #{}", id);
}
}
}
Comandos::Buscar { termo, apenas_titulo } => {
let escopo = if apenas_titulo { "títulos" } else { "todos os campos" };
println!("Buscando '{}' em {}", termo, escopo);
}
Comandos::Exportar { arquivo, formato_export } => {
println!("Exportando notas para {} (formato: {})",
arquivo.display(), formato_export);
}
Comandos::Importar { arquivo } => {
println!("Importando notas de {}", arquivo.display());
}
Comandos::Stats => {
println!("Estatísticas:");
println!(" Total de notas: 42");
println!(" Favoritas: 7");
println!(" Tags únicas: 15");
}
}
}
fn confirmar_remocao(ids: &[u64]) -> bool {
println!(
"Tem certeza que deseja remover {} nota(s)? [s/N]",
ids.len()
);
// Na prática, leria stdin aqui
false
}
Comparação com Alternativas
| Característica | Clap | structopt | argh | pico-args |
|---|---|---|---|---|
| API | Derive + Builder | Derive (deprecated) | Derive | Manual |
| Features | Completo | Completo | Mínimo | Mínimo |
| Help gerado | Sim (colorido) | Sim | Sim | Não |
| Subcomandos | Sim | Sim | Sim | Não |
| Completions | Sim (plugin) | Sim (plugin) | Não | Não |
| Validação | Rica | Rica | Básica | Manual |
| Tamanho binário | Maior | Maior | Menor | Mínimo |
| Compile time | Mais lento | Mais lento | Rápido | Rápido |
- Clap vs structopt: structopt foi absorvido pelo Clap 4. Use Clap diretamente.
- Clap vs argh: argh do Google é minimalista e compila rápido. Use argh para CLIs simples onde o tamanho do binário importa.
- Clap vs pico-args: pico-args é ultra-minimalista sem geração de help. Use apenas quando o tamanho é crítico.
Conclusão
O Clap é a solução completa para criar aplicações CLI em Rust. Sua API Derive torna a definição de interfaces de linha de comando tão simples quanto decorar structs com atributos, enquanto o framework cuida do parsing, validação, geração de help e autocompletion.
Para qualquer ferramenta CLI que vá além de um script trivial, o Clap é a escolha recomendada. A pequena penalidade em tamanho de binário e tempo de compilação é compensada pela robustez, ergonomia e qualidade profissional da interface gerada.
Próximos passos
- Combine com Config para carregar configuração de arquivos e variáveis de ambiente
- Use Tracing para adicionar logging estruturado à sua CLI
- Explore indicatif para barras de progresso e spinners
- Integre com Rusqlite para persistência local de dados