Clap Rust: Parser de Argumentos CLI Guia | Rust Brasil

Guia completo do Clap em Rust: derive API, argumentos, flags, subcomandos, validação e shell completion com exemplos práticos.

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ísticaClapstructoptarghpico-args
APIDerive + BuilderDerive (deprecated)DeriveManual
FeaturesCompletoCompletoMínimoMínimo
Help geradoSim (colorido)SimSimNão
SubcomandosSimSimSimNão
CompletionsSim (plugin)Sim (plugin)NãoNão
ValidaçãoRicaRicaBásicaManual
Tamanho binárioMaiorMaiorMenorMínimo
Compile timeMais lentoMais lentoRápidoRá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