Bem-vindo à aula 76 do nosso curso de Rust! Hoje vamos explorar a criação de aplicações de linha de comando (CLI) usando a crate clap, uma das bibliotecas mais populares e poderosas do ecossistema Rust. Se você já usou ferramentas como git, cargo ou rg, sabe como uma boa interface de linha de comando pode tornar a vida do usuário muito mais fácil. Com clap, você pode definir argumentos, subcomandos, flags e opções de forma declarativa, e ainda ganhar de brinde a geração automática de mensagens de ajuda e erro.

Nesta aula, vamos focar nos quatro pilares de uma CLI bem estruturada: parsing de argumentos, subcomandos, flags e help automático. Você verá como clap simplifica cada um desses aspectos, permitindo que você se concentre na lógica do seu programa em vez de reinventar a roda. Vamos começar com uma visão geral da crate e depois mergulhar em exemplos práticos.

Parsing de argumentos

O parsing de argumentos é o coração de qualquer CLI. Em Rust, sem uma biblioteca, você precisaria analisar manualmente o std::env::args(), o que rapidamente se torna tedioso e propenso a erros. clap resolve isso fornecendo uma API declarativa para definir quais argumentos seu programa aceita, seus tipos, valores padrão, restrições e muito mais.

Para começar, adicione clap ao seu Cargo.toml com o recurso derive, que permite usar macros para definir a estrutura de argumentos de forma concisa. A versão mais comum é a 4.x, então use clap = { version = "4.5", features = ["derive"] }.

Vamos criar um exemplo simples: um programa que aceita um nome (obrigatório) e uma idade (opcional). Com clap, definimos uma struct com campos anotados por atributos. O campo name é posicional e obrigatório, enquanto age é opcional e usa uma flag --age.

use clap::Parser;

/// Um programa de exemplo para cumprimentar alguém
#[derive(Parser)]
#[command(name = "greet", version = "1.0", about = "Diz olá")]
struct Cli {
    /// Nome da pessoa
    name: String,

    /// Idade da pessoa
    #[arg(short, long)]
    age: Option<u8>,
}

fn main() {
    let cli = Cli::parse();
    println!("Olá, {}!", cli.name);
    if let Some(age) = cli.age {
        println!("Você tem {} anos.", age);
    }
}

No código acima, #[derive(Parser)] gera automaticamente o parsing. O atributo #[arg(short, long)] faz com que a opção -a ou --age seja reconhecida. O campo name é posicional porque não tem nenhum atributo que o torne flag. O parsing é feito por Cli::parse(), que lê os argumentos da linha de comando e preenche a struct. Se algo estiver errado (por exemplo, faltar o nome), clap exibe uma mensagem de erro e sai do processo.

Uma vantagem importante é que clap lida com conversões de tipo automaticamente: u8 é convertido de string, e se o valor for inválido, um erro claro é mostrado. Além disso, você pode definir valores padrão, intervalos, etc. Por exemplo, para limitar a idade de 0 a 120, podemos usar o atributo value_parser com uma função de validação.

#[arg(short, long, value_parser = clap::value_parser!(u8).range(0..=120))]
age: Option<u8>,

Isso garante que apenas idades dentro do intervalo sejam aceitas, caso contrário, um erro é emitido.

Subcomandos

Subcomandos são essenciais para CLIs que possuem múltiplas operações, como git commit, git push, etc. clap suporta subcomandos de duas formas: usando uma enum com variantes que são structs (recomendado) ou usando o método subcommand() na definição do comando. A abordagem com enum é mais ergonômica.

Vamos ampliar nosso exemplo: um programa de tarefas que tem subcomandos add e list. O subcomando add recebe uma descrição da tarefa; list não recebe argumentos, apenas lista as tarefas. Usaremos uma enum Commands e anotaremos cada variante com #[derive(Subcommand)].

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "todo", version = "1.0", about = "Gerenciador de tarefas")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Adiciona uma nova tarefa
    Add {
        /// Descrição da tarefa
        description: String,
    },
    /// Lista todas as tarefas
    List,
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Add { description } => {
            println!("Adicionando tarefa: {}", description);
            // Aqui você poderia salvar em um arquivo, etc.
        }
        Commands::List => {
            println!("Listando tarefas...");
        }
    }
}

Com isso, o programa aceita todo add "Comprar pão" e todo list. O help gerado automaticamente mostrará os subcomandos e seus argumentos. Se você quiser que o subcomando tenha opções ou flags, basta adicionar campos na variante da enum.

Subcomandos podem ser aninhados indefinidamente. Para isso, você define uma enum para o sub-subcomando e a usa como campo dentro de uma variante. Isso é útil para CLIs complexas, como ferramentas de gerenciamento de projetos.

Flags

Flags são opções booleanas que ativam ou desativam comportamentos. No clap, você pode definir uma flag como um campo bool ou Option<bool> com o atributo #[arg(short, long)]. A diferença é que bool é uma flag simples (presença indica verdadeiro), enquanto Option<bool> permite que o usuário especifique explicitamente --flag true ou --flag false.

Vamos adicionar uma flag --verbose ao nosso programa de tarefas para exibir mais detalhes. No subcomando list, podemos ter uma flag para mostrar tarefas concluídas. Também podemos ter flags globais no comando principal, que são aplicáveis a todos os subcomandos.

#[derive(Parser)]
#[command(name = "todo", version = "1.0", about = "Gerenciador de tarefas")]
struct Cli {
    /// Modo verboso
    #[arg(short, long, global = true)]
    verbose: bool,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    Add {
        description: String,
    },
    List {
        /// Mostrar também tarefas concluídas
        #[arg(short, long)]
        all: bool,
    },
}

fn main() {
    let cli = Cli::parse();
    if cli.verbose {
        println!("Modo verboso ativado");
    }
    match cli.command {
        Commands::Add { description } => {
            println!("Adicionando tarefa: {}", description);
        }
        Commands::List { all } => {
            if all {
                println!("Listando todas as tarefas (incluindo concluídas)");
            } else {
                println!("Listando tarefas pendentes");
            }
        }
    }
}

No exemplo, a flag --verbose é global, ou seja, pode ser usada antes ou depois do subcomando. A flag --all é específica do subcomando list. Você pode combinar flags com argumentos posicionais e opções. O clap também suporta flags com múltiplas ocorrências, como -v -v, usando count.

Outra funcionalidade útil é a flag --version que é gerada automaticamente se você especificar version no atributo #[command]. Da mesma forma, --help é automático.

Help automático

Um dos maiores benefícios do clap é a geração automática de mensagens de help e erro. Isso significa que você não precisa escrever manualmente textos de ajuda; basta documentar seus argumentos com comentários (que viram descrições) e o clap cria uma saída formatada e colorida (se o terminal suportar).

Para personalizar o help, você pode usar atributos como about, long_about, help, long_help nos comandos e argumentos. Por exemplo, no struct Cli, o atributo about define a descrição exibida no help geral. Nos argumentos, o comentário de documentação (///) é usado como help curto, e você pode fornecer um help mais longo com #[arg(help = "...")].

Vamos rodar nosso programa todo com --help para ver o resultado:

$ cargo run -- --help
Uso: todo [OPÇÕES] <COMANDO>

Comandos:
  add   Adiciona uma nova tarefa
  list  Lista todas as tarefas
  help  Exibe ajuda para um comando ou subcomando

Opções:
  -v, --verbose  Modo verboso
  -h, --help     Exibe a ajuda
  -V, --version  Exibe a versão

E para um subcomando:

$ cargo run -- add --help
Uso: todo add [OPÇÕES] <DESCRIÇÃO>

Argumentos:
  <DESCRIÇÃO>  Descrição da tarefa

Opções:
  -h, --help  Exibe a ajuda

Note que o help é gerado automaticamente e inclui todos os argumentos, opções e subcomandos. Você pode personalizar a aparência usando o #[command(help_template = "...")] para controlar o formato, mas o padrão já é excelente.

Além disso, o clap gera mensagens de erro claras quando argumentos inválidos são fornecidos. Por exemplo, se faltar um argumento obrigatório, ele mostra a mensagem e o uso correto.

Boas práticas e observações

Ao construir CLIs com clap, lembre-se de:

  • Use a abordagem derive sempre que possível, pois é mais concisa e menos propensa a erros que a API builder.
  • Documente bem seus argumentos com comentários, pois eles aparecem no help.
  • Prefira nomes longos e curtos consistentes (ex.: -v e --verbose).
  • Use global = true para flags que se aplicam a todos os subcomandos.
  • Considere usar value_parser para validar entradas e melhorar a experiência do usuário.
  • Teste sua CLI com diferentes combinações de argumentos para garantir que o help e os erros sejam claros.

Com essas práticas, sua CLI será robusta e amigável.

Referências

Exercícios

  1. Exercício 1: Crie um programa que aceite dois números posicionais e uma flag --sum para exibir a soma, ou --product para o produto. Use clap derive.
  2. ✓ Resposta:
    use clap::Parser;
    
    #[derive(Parser)]
    #[command(name = "calc", version = "1.0", about = "Calculadora simples")]
    struct Cli {
        a: f64,
        b: f64,
        #[arg(long)]
        sum: bool,
        #[arg(long)]
        product: bool,
    }
    
    fn main() {
        let cli = Cli::parse();
        if cli.sum {
            println!("Soma: {}", cli.a + cli.b);
        } else if cli.product {
            println!("Produto: {}", cli.a * cli.b);
        } else {
            println!("Nenhuma operação especificada");
        }
    }
    
  3. Exercício 2: Implemente um subcomando greet que aceita um nome e uma flag --formal para usar "Caro(a)" em vez de "Olá".
  4. ✓ Resposta:
    use clap::{Parser, Subcommand};
    
    #[derive(Parser)]
    struct Cli {
        #[command(subcommand)]
        command: Commands,
    }
    
    #[derive(Subcommand)]
    enum Commands {
        /// Cumprimenta uma pessoa
        Greet {
            /// Nome da pessoa
            name: String,
            /// Usar saudação formal
            #[arg(short, long)]
            formal: bool,
        },
    }
    
    fn main() {
        let cli = Cli::parse();
        match cli.command {
            Commands::Greet { name, formal } => {
                if formal {
                    println!("Caro(a) {}", name);
                } else {
                    println!("Olá {}", name);
                }
            }
        }
    }
    
  5. Exercício 3: Adicione uma flag global --verbose ao programa do exercício 2 e faça com que ela exiba uma mensagem extra quando ativada.
  6. ✓ Resposta:
    use clap::{Parser, Subcommand};
    
    #[derive(Parser)]
    struct Cli {
        #[arg(short, long, global = true)]
        verbose: bool,
        #[command(subcommand)]
        command: Commands,
    }
    
    #[derive(Subcommand)]
    enum Commands {
        Greet {
            name: String,
            #[arg(short, long)]
            formal: bool,
        },
    }
    
    fn main() {
        let cli = Cli::parse();
        if cli.verbose {
            println!("Modo verboso ativado");
        }
        match cli.command {
            Commands::Greet { name, formal } => {
                if formal {
                    println!("Caro(a) {}", name);
                } else {
                    println!("Olá {}", name);
                }
            }
        }
    }
    
  7. Exercício 4: Use value_parser para limitar um argumento --level a valores entre 1 e 10 (inclusive).
  8. ✓ Resposta:
    use clap::Parser;
    
    #[derive(Parser)]
    struct Cli {
        #[arg(long, value_parser = clap::value_parser!(u8).range(1..=10))]
        level: Option<u8>,
    }
    
    fn main() {
        let cli = Cli::parse();
        if let Some(l) = cli.level {
            println!("Nível: {}", l);
        }
    }
    
  9. Exercício 5: Crie uma CLI com três subcomandos: create, read e delete, cada um com um argumento id (inteiro). Use uma enum e implemente a lógica de impressão.
  10. ✓ Resposta:
    use clap::{Parser, Subcommand};
    
    #[derive(Parser)]
    struct Cli {
        #[command(subcommand)]
        command: Commands,
    }
    
    #[derive(Subcommand)]
    enum Commands {
        Create { id: u32 },
        Read { id: u32 },
        Delete { id: u32 },
    }
    
    fn main() {
        let cli = Cli::parse();
        match cli.command {
            Commands::Create { id } => println!("Criando recurso com id {}", id),
            Commands::Read { id } => println!("Lendo recurso com id {}", id),
            Commands::Delete { id } => println!("Deletando recurso com id {}", id),
        }
    }