Introdução
Criar aplicações de linha de comando (CLI) é um dos casos de uso mais populares do Rust, e o Clap é a biblioteca padrão para parsing de argumentos. Com a versão 4, o Clap consolidou a derive API que torna a criação de CLIs tão simples quanto definir uma struct.
Historicamente, existia o StructOpt como uma camada derive separada sobre o Clap. Desde o Clap 3, a derive API foi incorporada diretamente ao Clap, tornando o StructOpt obsoleto. Se você está usando StructOpt, este artigo também serve como guia de migração.
Neste guia, vamos cobrir desde o básico até funcionalidades avançadas como subcomandos, validação customizada, auto-completions e output colorido.
Dependências no Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
Para funcionalidades extras:
[dependencies]
clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] }
clap_complete = "4" # Auto-completions para shells
anstyle = "1" # Cores no help
Tabela: Clap 4 vs StructOpt
| Característica | Clap 4 (Derive) | StructOpt |
|---|---|---|
| Status | Ativo, versão atual | Deprecado |
| API | #[derive(Parser)] | #[derive(StructOpt)] |
| Atributo | #[arg(...)] e #[command(...)] | #[structopt(...)] |
| Subcomandos | #[derive(Subcommand)] | #[derive(StructOpt)] com enum |
| Validação | value_parser | validator |
| Completions | clap_complete | structopt::clap::Shell |
| Performance | Otimizada | Overhead adicional |
CLI Básica
use clap::Parser;
/// Ferramenta para gerenciar tarefas
#[derive(Parser, Debug)]
#[command(name = "tarefas")]
#[command(version, about, long_about = None)]
struct Cli {
/// Nome da tarefa a ser criada
nome: String,
/// Prioridade da tarefa (1-5)
#[arg(short, long, default_value_t = 3)]
prioridade: u8,
/// Marcar como concluída
#[arg(short, long)]
concluida: bool,
/// Tags da tarefa
#[arg(short, long)]
tags: Vec<String>,
/// Modo verbose
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
fn main() {
let cli = Cli::parse();
println!("{cli:#?}");
match cli.verbose {
0 => println!("Tarefa: {}", cli.nome),
1 => println!("Tarefa: {} (prioridade: {})", cli.nome, cli.prioridade),
_ => println!("Tarefa: {:?}", cli),
}
}
Uso:
$ tarefas "Estudar Rust" --prioridade 5 -t rust -t estudo -v
Tarefa: Estudar Rust (prioridade: 5)
Help gerado automaticamente:
Ferramenta para gerenciar tarefas
Usage: tarefas [OPTIONS] <NOME>
Arguments:
<NOME> Nome da tarefa a ser criada
Options:
-p, --prioridade <PRIORIDADE> Prioridade da tarefa (1-5) [default: 3]
-c, --concluida Marcar como concluída
-t, --tags <TAGS> Tags da tarefa
-v, --verbose... Modo verbose
-h, --help Print help
-V, --version Print version
Subcomandos
Subcomandos são definidos como enums com #[derive(Subcommand)]:
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "proj")]
#[command(about = "Gerenciador de projetos Rust")]
struct Cli {
#[command(subcommand)]
comando: Comandos,
}
#[derive(Subcommand, Debug)]
enum Comandos {
/// Criar um novo projeto
Novo {
/// Nome do projeto
nome: String,
/// Template a usar
#[arg(short, long, default_value = "basico")]
template: String,
},
/// Compilar o projeto
Build {
/// Compilar em modo release
#[arg(short, long)]
release: bool,
/// Diretório de saída
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Executar testes
Test {
/// Filtro de testes
filtro: Option<String>,
/// Executar testes ignorados
#[arg(long)]
include_ignored: bool,
},
/// Publicar no crates.io
Publish {
/// Dry run (sem publicar de verdade)
#[arg(long)]
dry_run: bool,
},
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comandos::Novo { nome, template } => {
println!("Criando projeto '{nome}' com template '{template}'");
}
Comandos::Build { release, output } => {
let modo = if release { "release" } else { "debug" };
println!("Compilando em modo {modo}");
if let Some(path) = output {
println!("Saída: {}", path.display());
}
}
Comandos::Test { filtro, include_ignored } => {
println!("Executando testes");
if let Some(f) = filtro {
println!("Filtro: {f}");
}
if include_ignored {
println!("Incluindo testes ignorados");
}
}
Comandos::Publish { dry_run } => {
if dry_run {
println!("Dry run: verificando publicação...");
} else {
println!("Publicando...");
}
}
}
}
Validação de Argumentos
Validação com value_parser
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser, Debug)]
struct Cli {
/// Porta do servidor (1024-65535)
#[arg(short, long, value_parser = clap::value_parser!(u16).range(1024..=65535))]
porta: u16,
/// Arquivo de entrada (deve existir)
#[arg(short, long, value_parser = validar_arquivo_existe)]
entrada: PathBuf,
/// Nível de log
#[arg(short, long, value_parser = ["debug", "info", "warn", "error"])]
log_level: String,
/// Email do usuário
#[arg(short, long, value_parser = validar_email)]
email: String,
}
fn validar_arquivo_existe(s: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(s);
if path.exists() {
Ok(path)
} else {
Err(format!("Arquivo não encontrado: {s}"))
}
}
fn validar_email(s: &str) -> Result<String, String> {
if s.contains('@') && s.contains('.') {
Ok(s.to_string())
} else {
Err(format!("Email inválido: {s}"))
}
}
fn main() {
let cli = Cli::parse();
println!("{cli:#?}");
}
Valores de Enums
use clap::{Parser, ValueEnum};
#[derive(Debug, Clone, ValueEnum)]
enum Formato {
Json,
Yaml,
Toml,
Csv,
}
#[derive(Parser, Debug)]
struct Cli {
/// Formato de saída
#[arg(short, long, value_enum, default_value_t = Formato::Json)]
formato: Formato,
}
fn main() {
let cli = Cli::parse();
match cli.formato {
Formato::Json => println!("Saída em JSON"),
Formato::Yaml => println!("Saída em YAML"),
Formato::Toml => println!("Saída em TOML"),
Formato::Csv => println!("Saída em CSV"),
}
}
Variáveis de Ambiente
use clap::Parser;
#[derive(Parser, Debug)]
struct Cli {
/// URL do banco de dados
#[arg(long, env = "DATABASE_URL")]
database_url: String,
/// Token de API
#[arg(long, env = "API_TOKEN", hide_env_values = true)]
api_token: String,
/// Porta do servidor
#[arg(short, long, env = "PORT", default_value_t = 8080)]
porta: u16,
}
fn main() {
let cli = Cli::parse();
println!("DB: {}", cli.database_url);
println!("Porta: {}", cli.porta);
}
Auto-Completions para Shells
Gerar scripts de auto-complete para bash, zsh, fish e PowerShell:
use clap::{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 a aplicação
Run,
/// Gerar completions para o shell
Completions {
/// Shell alvo
#[arg(value_enum)]
shell: Shell,
},
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comandos::Run => {
println!("Executando...");
}
Comandos::Completions { shell } => {
let mut cmd = <Cli as clap::CommandFactory>::command();
generate(shell, &mut cmd, "minha-cli", &mut io::stdout());
}
}
}
Uso:
# Gerar completions para bash
minha-cli completions bash > /etc/bash_completion.d/minha-cli
# Para zsh
minha-cli completions zsh > ~/.zfunc/_minha-cli
# Para fish
minha-cli completions fish > ~/.config/fish/completions/minha-cli.fish
Output Colorido
Para output colorido no terminal, combine com crates como colored ou owo-colors:
[dependencies]
clap = { version = "4", features = ["derive"] }
colored = "2"
indicatif = "0.17" # Barras de progresso
use clap::Parser;
use colored::Colorize;
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
comando: Comando,
}
#[derive(clap::Subcommand)]
enum Comando {
/// Verificar status do projeto
Status,
/// Compilar o projeto
Build,
}
fn main() {
let cli = Cli::parse();
match cli.comando {
Comando::Status => {
println!("{} Verificando status...", "●".blue());
println!(" {} 3 testes passando", "✓".green());
println!(" {} 1 warning", "!".yellow());
println!(" {} Nenhum erro", "✓".green());
}
Comando::Build => {
println!("{}", "Compilando projeto...".bold());
println!(" {} src/main.rs", "Compilando".green().bold());
println!(" {} Build completo em 2.3s", "Sucesso:".green().bold());
}
}
}
Barras de Progresso com indicatif
use indicatif::{ProgressBar, ProgressStyle};
use std::thread;
use std::time::Duration;
fn main() {
let pb = ProgressBar::new(100);
pb.set_style(
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})"
)
.unwrap()
.progress_chars("█▓░"),
);
for _ in 0..100 {
pb.inc(1);
thread::sleep(Duration::from_millis(30));
}
pb.finish_with_message("Concluído!");
}
Migração do StructOpt para Clap 4
Se você tem um projeto usando StructOpt, aqui está o guia de migração:
Dependências
# Antes (StructOpt)
[dependencies]
structopt = "0.3"
# Depois (Clap 4)
[dependencies]
clap = { version = "4", features = ["derive"] }
Derives e Atributos
// Antes (StructOpt)
use structopt::StructOpt;
#[derive(StructOpt)]
#[structopt(name = "minha-cli", about = "Descrição")]
struct Cli {
#[structopt(short, long, default_value = "8080")]
porta: u16,
#[structopt(subcommand)]
cmd: Comando,
}
#[derive(StructOpt)]
enum Comando {
#[structopt(about = "Executar")]
Run {
#[structopt(short, long)]
verbose: bool,
},
}
fn main() {
let cli = Cli::from_args();
}
// Depois (Clap 4)
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "minha-cli", about = "Descrição")]
struct Cli {
#[arg(short, long, default_value_t = 8080)]
porta: u16,
#[command(subcommand)]
cmd: Comando,
}
#[derive(Subcommand)]
enum Comando {
/// Executar
Run {
#[arg(short, long)]
verbose: bool,
},
}
fn main() {
let cli = Cli::parse();
}
Principais mudanças na migração:
| StructOpt | Clap 4 |
|---|---|
#[derive(StructOpt)] | #[derive(Parser)] ou #[derive(Subcommand)] |
#[structopt(...)] | #[arg(...)] para args, #[command(...)] para command |
Cli::from_args() | Cli::parse() |
default_value = "8080" | default_value_t = 8080 (tipado) |
validator = "fn" | value_parser = fn |
possible_values | value_enum ou value_parser |
Padrões e Boas Práticas
- Use doc comments (
///) como descrições — eles aparecem no--help - Sempre defina
#[command(version)]para que--versionfuncione - Use
value_enumpara argumentos com valores finitos - Prefira
default_value_tsobredefault_valuepara tipos não-String - Use
envpara configurações que também podem vir de variáveis de ambiente - Gere completions para melhorar a experiência do usuário
- Use
hide = truepara flags internas/de debug
use clap::Parser;
/// Minha ferramenta incrível para gerenciar deploys
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
/// Ambiente alvo
#[arg(short, long, value_enum)]
ambiente: Ambiente,
/// Flags de debug (interno)
#[arg(long, hide = true)]
debug_flags: Option<String>,
}
#[derive(Clone, clap::ValueEnum)]
enum Ambiente {
Dev,
Staging,
Producao,
}
Conclusão
O Clap 4 é a ferramenta definitiva para criar CLIs em Rust. A derive API torna o processo declarativo e ergonômico, enquanto features como validação, subcomandos, completions e variáveis de ambiente cobrem praticamente qualquer necessidade. Se você ainda usa StructOpt, a migração para Clap 4 é simples e vale o esforço.