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