CLI apps com Clap: parsing de argumentos elegante
1. Introdução ao Clap e configuração inicial
Clap (Command Line Argument Parser) é a crate mais popular do ecossistema Rust para parsing de argumentos de linha de comando. Com sua abordagem declarativa usando macros derive, você pode definir interfaces CLI complexas com mínimo esforço e máxima segurança de tipos.
Para começar, adicione Clap ao seu projeto:
cargo add clap --features derive
A estrutura básica de um CLI com Clap utiliza a trait Parser e o macro #[derive(Parser)]:
use clap::Parser;
#[derive(Parser)]
#[command(name = "meuapp", version = "1.0", about = "Um exemplo simples")]
struct Args {
/// Nome do usuário
nome: String,
}
fn main() {
let args = Args::parse();
println!("Olá, {}!", args.nome);
}
Ao executar cargo run -- João, o programa imprime "Olá, João!". O Clap automaticamente gera mensagens de ajuda com --help e -h.
2. Definindo argumentos posicionais e opcionais
Argumentos posicionais são aqueles que devem aparecer em ordem específica na linha de comando. Já os opcionais são identificados por flags como --flag ou -f.
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser)]
struct Args {
/// Arquivo de entrada (posicional)
input: PathBuf,
/// Arquivo de saída (opcional com valor)
#[arg(short = 'o', long = "output")]
output: Option<PathBuf>,
/// Modo verbose (flag booleana)
#[arg(short, long)]
verbose: bool,
/// Número de threads (com valor padrão)
#[arg(short, long, default_value_t = 4)]
threads: u32,
}
fn main() {
let args = Args::parse();
println!("Input: {:?}", args.input);
if let Some(out) = args.output {
println!("Output: {:?}", out);
}
println!("Verbose: {}, Threads: {}", args.verbose, args.threads);
}
Tipos suportados nativamente incluem String, u32, bool, PathBuf, além de tipos customizados que implementam FromStr. Para tipos personalizados, você pode usar value_parser:
#[derive(Debug, Clone)]
enum Modo {
Leitura,
Escrita,
}
impl std::str::FromStr for Modo {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"leitura" => Ok(Modo::Leitura),
"escrita" => Ok(Modo::Escrita),
_ => Err(format!("Modo inválido: {}", s)),
}
}
}
#[derive(Parser)]
struct Args {
#[arg(value_parser = clap::value_parser!(Modo))]
modo: Modo,
}
3. Subcomandos: organizando funcionalidades complexas
Subcomandos permitem criar ferramentas com múltiplas operações, similar ao git add, git commit, etc. Use #[derive(Subcommand)] em um enum:
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Adiciona arquivos ao staging
Add {
/// Arquivos para adicionar
files: Vec<String>,
},
/// Cria um novo commit
Commit {
#[arg(short, long)]
message: String,
},
/// Envia para repositório remoto
Push {
#[arg(default_value = "origin")]
remote: String,
#[arg(default_value = "main")]
branch: String,
},
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Commands::Add { files } => println!("Adicionando {:?}", files),
Commands::Commit { message } => println!("Commit: {}", message),
Commands::Push { remote, branch } => {
println!("Enviando para {}/{}", remote, branch);
}
}
}
4. Personalizando ajuda e versão
O Clap gera automaticamente mensagens de ajuda, mas você pode customizá-las extensivamente:
use clap::Parser;
#[derive(Parser)]
#[command(
name = "ferramenta",
version = "1.0.0",
about = "Ferramenta CLI para processamento de dados",
long_about = "Uma ferramenta completa para processamento de dados\n\
com suporte a múltiplos formatos de entrada e saída.",
help_template = "{name} v{version}\n{about}\n\n\
Uso: {usage}\n\n\
{all-args}\n\n\
Documentação: https://exemplo.com/docs"
)]
struct Args {
/// Arquivo de entrada
#[arg(short, long)]
input: String,
/// Ativar modo debug
#[arg(short, long)]
debug: bool,
}
5. Validação avançada de entrada
Clap oferece diversas formas de validar e restringir argumentos:
use clap::Parser;
#[derive(Parser)]
struct Args {
/// Porta do servidor (1-65535)
#[arg(short, long, default_value_t = 8080, value_parser = clap::value_parser!(u16).range(1..))]
port: u16,
/// Arquivo de configuração (obrigatório se não usar --default)
#[arg(short, long, conflicts_with = "default")]
config: Option<String>,
/// Usar configuração padrão
#[arg(short, long)]
default: bool,
/// Modo de operação (exige --user)
#[arg(short, long, requires = "user")]
admin: bool,
/// Nome do usuário
#[arg(short, long)]
user: Option<String>,
}
fn main() {
let args = Args::parse();
// Validação customizada adicional
if args.admin && args.user.is_none() {
eprintln!("Erro: modo admin requer --user");
std::process::exit(1);
}
}
6. Parsing com builder pattern (alternativa ao derive)
Para casos que exigem controle mais fino, o Clap oferece a API builder:
use clap::{Command, Arg};
fn main() {
let matches = Command::new("app")
.version("1.0")
.author("Autor")
.about("Exemplo com builder pattern")
.arg(
Arg::new("input")
.short('i')
.long("input")
.required(true)
.help("Arquivo de entrada"),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.default_value("output.txt")
.help("Arquivo de saída"),
)
.get_matches();
let input = matches.get_one::<String>("input").unwrap();
let output = matches.get_one::<String>("output").unwrap();
println!("Input: {}, Output: {}", input, output);
}
Quando usar cada abordagem?
- Derive: ideal para 90% dos casos, mais conciso e seguro
- Builder: necessário para parsing dinâmico, argumentos condicionais complexos ou quando você precisa construir a interface em runtime
7. Tratamento de erros e saída elegante
Clap lida com a maioria dos erros automaticamente, mas você pode customizar:
use clap::{Parser, error::ErrorKind};
#[derive(Parser)]
struct Args {
#[arg(short, long)]
numero: i32,
}
fn main() {
let args = match Args::try_parse() {
Ok(a) => a,
Err(e) => {
if e.kind() == ErrorKind::InvalidValue {
eprintln!("Erro: valor inválido fornecido");
e.exit();
} else {
e.exit(); // Mensagem padrão do Clap
}
}
};
if args.numero < 0 {
eprintln!("Erro: número negativo não permitido");
std::process::exit(1);
}
}
8. Exemplo completo: mini gerenciador de tarefas
Vamos criar um gerenciador de tarefas simples:
// src/main.rs
use clap::Parser;
mod cli;
mod commands;
fn main() {
let cli = cli::Cli::parse();
commands::executar(cli);
}
// src/cli.rs
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "tarefas", version = "1.0", about = "Gerenciador de tarefas")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Adiciona nova tarefa
Add {
/// Descrição da tarefa
descricao: String,
},
/// Lista todas as tarefas
List,
/// Remove tarefa por ID
Remove {
/// ID da tarefa
id: usize,
},
}
// src/commands.rs
use std::fs;
use crate::cli::{Cli, Commands};
#[derive(serde::Serialize, serde::Deserialize)]
struct Tarefa {
id: usize,
descricao: String,
}
pub fn executar(cli: Cli) {
match &cli.command {
Commands::Add { descricao } => adicionar(descricao),
Commands::List => listar(),
Commands::Remove { id } => remover(*id),
}
}
fn carregar_tarefas() -> Vec<Tarefa> {
fs::read_to_string("tarefas.json")
.ok()
.and_then(|d| serde_json::from_str(&d).ok())
.unwrap_or_default()
}
fn salvar_tarefas(tarefas: &[Tarefa]) {
let dados = serde_json::to_string_pretty(tarefas).unwrap();
fs::write("tarefas.json", dados).unwrap();
}
fn adicionar(descricao: &str) {
let mut tarefas = carregar_tarefas();
let id = tarefas.len() + 1;
tarefas.push(Tarefa {
id,
descricao: descricao.to_string(),
});
salvar_tarefas(&tarefas);
println!("Tarefa #{} adicionada: {}", id, descricao);
}
fn listar() {
let tarefas = carregar_tarefas();
if tarefas.is_empty() {
println!("Nenhuma tarefa encontrada.");
} else {
for t in &tarefas {
println!("#{}: {}", t.id, t.descricao);
}
}
}
fn remover(id: usize) {
let mut tarefas = carregar_tarefas();
if let Some(pos) = tarefas.iter().position(|t| t.id == id) {
tarefas.remove(pos);
salvar_tarefas(&tarefas);
println!("Tarefa #{} removida.", id);
} else {
eprintln!("Erro: tarefa #{} não encontrada.", id);
}
}
Este exemplo demonstra boas práticas como separação de concerns, uso de serde para serialização e tratamento adequado de erros.
Referências
- Documentação oficial do Clap — Documentação completa da crate com exemplos para todas as funcionalidades
- Clap GitHub Repository — Código fonte, issues e exemplos avançados da comunidade
- Rust CLI Book - Argument Parsing — Guia oficial sobre desenvolvimento CLI em Rust, incluindo seção sobre Clap
- Clap Derive Tutorial — Tutorial passo a passo focado no uso do macro derive
- Awesome Rust - CLI Libraries — Lista curada de bibliotecas CLI em Rust, incluindo alternativas e complementos ao Clap