Projeto: CLI com clap em Rust
Na página de Argumentos de linha de comando, você leu os argumentos à mão com std::env::args(). Isso funciona para um ou dois argumentos. Mas ferramentas de verdade precisam de opções como --precision 3, formas curtas como -p 3, uma página de --help, boas mensagens de erro e subcomandos como git commit e git push. Escrever tudo isso à mão dá muito código chato e fácil de errar.
O clap (sigla em inglês para Command Line Argument Parser) é o crate que quase toda CLI (ferramenta de linha de comando) em Rust usa para isso. Você descreve os seus argumentos como uma struct normal do Rust, e o clap escreve para você o parser, o texto de ajuda e as mensagens de erro. Neste projeto, você vai construir o convert, um pequeno conversor de unidades com dois subcomandos: temp, para temperaturas, e length, para comprimentos.
O que você vai construir
Seção intitulada “O que você vai construir”Aqui está a ferramenta pronta, rodando num terminal. Todas as saídas desta página vêm de rodar o programa de verdade.
$ cargo run -q -- temp 100 --from celsius --to fahrenheit100 °C = 212.00 °F$ cargo run -q -- temp -40 -f c -t f-40 °C = -40.00 °F$ cargo run -q -- length 5 km mi5 km = 3.11 mi$ cargo run -q -- -p 0 length 1 mi ft1 mi = 5280 ft$ cargo run -q -- temp 20 --from rankine --to cerror: invalid value 'rankine' for '--from <FROM>' [possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.Tudo o que vem depois de -- vai para o seu programa em vez de ir para o Cargo, e o -q (de quiet, silencioso) esconde as linhas “Compiling…” do próprio Cargo. E aquela última mensagem de erro? Não fomos nós que escrevemos. Foi o clap. Ela diz que rankine é um valor inválido (invalid value) para --from e lista os valores possíveis (possible values). As mensagens prontas do clap são sempre em inglês.
Passo 1: Crie o projeto e adicione o clap
Seção intitulada “Passo 1: Crie o projeto e adicione o clap”cargo new convertcd convertcargo add clap --features deriveA feature derive é a parte importante. Ela ativa o #[derive(Parser)], que é o jeito mais amigável de usar o clap. Agora o seu Cargo.toml contém:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }(O seu número de versão pode ser um pouco mais novo. Qualquer versão que comece com 4 funciona para esta página. Veja Cargo e crates se o cargo add for novidade para você.)
Passo 2: O seu primeiro parser
Seção intitulada “Passo 2: O seu primeiro parser”Vamos começar com uma ferramenta que só converte Celsius para Fahrenheit. Você escreve uma struct com um campo para cada argumento e coloca #[derive(Parser)] nela. Substitua o src/main.rs por:
use clap::Parser;
/// Converte uma temperatura de Celsius para Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// A temperatura em Celsius celsius: f64,
/// Quantas casas decimais mostrar #[arg(short, long, default_value_t = 2)] precision: usize,
/// Também mostra a fórmula que foi usada #[arg(short, long)] verbose: bool,}
fn main() { let cli = Cli::parse(); let fahrenheit = cli.celsius * 9.0 / 5.0 + 32.0; println!("{} °C = {:.*} °F", cli.celsius, cli.precision, fahrenheit); if cli.verbose { println!("(fórmula usada: °F = °C × 9/5 + 32)"); }}$ cargo run -q -- 21.521.5 °C = 70.70 °F$ cargo run -q -- 100 --precision 1 --verbose100 °C = 212.0 °F(fórmula usada: °F = °C × 9/5 + 32)O clap lê cada campo e decide que tipo de argumento ele é:
celsiusnão tem nenhuma opção#[arg(...)], então é um argumento posicional: o primeiro valor simples que você digita. Como o tipo dele éf64, o clap também converte o texto"21.5"em número para você.#[arg(short, long)]cria uma opção com um nome curto e um nome longo, tirados do nome do campo:-p/--precision. Odefault_value_t = 2é usado quando você não passa a opção.- Um campo
boolvira uma flag: ele étruese--verbose(ou-v) estiver presente, efalsese não estiver. - Um campo
Option<T>(que não usamos aqui) seria um argumento que você pode deixar de fora por completo.
O {:.*} no println! quer dizer “use o próximo argumento como o número de casas decimais”. É assim que o --precision chega até a saída.
O Cli::parse() lê a linha de comando de verdade. O Rust Playground não tem linha de comando, então, para testar o parser lá, use Cli::parse_from com uma lista de argumentos de faz de conta. O primeiro item é sempre o nome do programa, igualzinho ao std::env::args():
use clap::Parser;
/// Converte uma temperatura de Celsius para Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// A temperatura em Celsius celsius: f64,
/// Quantas casas decimais mostrar #[arg(short, long, default_value_t = 2)] precision: usize,
/// Também mostra a fórmula que foi usada #[arg(short, long)] verbose: bool,}
fn main() { // Faz de conta que o usuário digitou: convert 100 --precision 1 --verbose let cli = Cli::parse_from(["convert", "100", "--precision", "1", "--verbose"]); println!("{cli:?}");
let fahrenheit = cli.celsius * 9.0 / 5.0 + 32.0; println!("{} °C = {:.*} °F", cli.celsius, cli.precision, fahrenheit);}Saída:
Cli { celsius: 100.0, precision: 1, verbose: true }100 °C = 212.0 °FExperimente mudar os argumentos de faz de conta: tire o "--verbose", ou troque "--precision" por "-p".
Passo 3: Texto de ajuda a partir dos comentários de documentação
Seção intitulada “Passo 3: Texto de ajuda a partir dos comentários de documentação”Reparou nas linhas /// da struct? Elas são comentários de documentação, e o clap transforma esses comentários na página de ajuda. O comentário acima da struct descreve o programa inteiro; o comentário acima de cada campo descreve aquele argumento. Você ganha o --help (e o -h) sem escrever mais nada:
$ cargo run -q -- --helpConverte uma temperatura de Celsius para Fahrenheit
Usage: convert [OPTIONS] <CELSIUS>
Arguments: <CELSIUS> A temperatura em Celsius
Options: -p, --precision <PRECISION> Quantas casas decimais mostrar [default: 2] -v, --verbose Também mostra a fórmula que foi usada -h, --help Print help -V, --version Print version$ cargo run -q -- --versionconvert 1.0Os textos que vieram dos seus comentários aparecem em português. Já os títulos e textos fixos do clap ficam em inglês: Usage (uso), Arguments (argumentos), Options (opções), [default: 2] (padrão: 2), Print help (mostrar a ajuda) e Print version (mostrar a versão).
A linha #[command(name = "convert", version = "1.0")] define o nome e a versão que aparecem aqui. Se você escrever só #[command(version)], o clap pega a versão do seu Cargo.toml.
O clap também explica os erros para você:
$ cargo run -q --error: the following required arguments were not provided: <CELSIUS>
Usage: convert <CELSIUS>
For more information, try '--help'.$ cargo run -q -- abcerror: invalid value 'abc' for '<CELSIUS>': invalid float literal
For more information, try '--help'.O primeiro erro diz que um argumento obrigatório não foi informado (required arguments were not provided). O segundo diz que abc não é um número decimal válido (invalid float literal). Nos dois casos, o clap sugere rodar --help para ver mais informações.
Um bom texto de ajuda é um presente para o seu eu do futuro. Escreva os comentários de documentação como se alguém estivesse vendo a sua ferramenta pela primeira vez, porque um dia essa pessoa vai ser você.
Passo 4: Subcomandos
Seção intitulada “Passo 4: Subcomandos”Agora vamos aceitar comprimentos também. Ferramentas como o git e o cargo usam subcomandos (cargo build, cargo test) para agrupar tarefas diferentes. No clap, um subcomando é um enum com #[derive(Subcommand)]. Cada variante é um subcomando, e os campos dela são os argumentos daquele subcomando:
use clap::{Parser, Subcommand};
/// Um pequeno conversor de unidades para temperaturas e comprimentos#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Quantas casas decimais mostrar #[arg(short, long, default_value_t = 2, global = true)] precision: usize,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Converte uma temperatura Temp { /// A temperatura a converter value: f64,
/// A unidade de origem #[arg(short, long)] from: String,
/// A unidade de destino #[arg(short, long)] to: String, }, /// Converte um comprimento Length { /// O comprimento a converter value: f64,
/// A unidade de origem from: String,
/// A unidade de destino to: String, },}
fn main() { let cli = Cli::parse_from(["convert", "temp", "100", "--from", "c", "--to", "f"]); println!("{cli:?}");
let cli = Cli::parse_from(["convert", "length", "5", "km", "mi", "-p", "1"]); println!("{cli:?}");
// Você descobre qual subcomando foi usado com um match normal. match cli.command { Command::Temp { value, from, to } => println!("temperatura: {value} {from} -> {to}"), Command::Length { value, from, to } => println!("comprimento: {value} {from} -> {to}"), }}Saída:
Cli { precision: 2, command: Temp { value: 100.0, from: "c", to: "f" } }Cli { precision: 1, command: Length { value: 5.0, from: "km", to: "mi" } }comprimento: 5 km -> miAlguns detalhes:
#[command(subcommand)]no campocommanddiz ao clap: “este campo guarda o subcomando escolhido”.- O nome da variante
Tempvira o subcomandotemp(o clap passa para minúsculas por você). - O
tempusa opções com nome (--from c --to f), enquanto olengthusa argumentos posicionais (5 km mi). Os dois estilos são comuns, e assim você vê cada um deles. global = truedeixa o--precisionaparecer antes ou depois do subcomando. Sem isso, o-p 1só seria aceito antes dolength.
A página de ajuda agora lista os subcomandos (Commands), e cada subcomando tem a sua própria ajuda:
$ cargo run -q -- --helpUm pequeno conversor de unidades para temperaturas e comprimentos
Usage: convert [OPTIONS] <COMMAND>
Commands: temp Converte uma temperatura length Converte um comprimento help Print this message or the help of the given subcommand(s)
Options: -p, --precision <PRECISION> Quantas casas decimais mostrar [default: 2] -h, --help Print help -V, --version Print version$ cargo run -q -- temp --helpConverte uma temperatura
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> A temperatura a converter
Options: -f, --from <FROM> A unidade de origem -p, --precision <PRECISION> Quantas casas decimais mostrar [default: 2] -t, --to <TO> A unidade de destino -h, --help Print helpMas tem um problema. As unidades são Strings simples, então o clap aceita qualquer coisa:
$ cargo run -q -- temp 20 --from rankine --to cCli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }Teríamos que conferir cada unidade à mão depois. Vamos deixar o clap fazer isso.
Passo 5: Enums e validação com ValueEnum
Seção intitulada “Passo 5: Enums e validação com ValueEnum”Quando um argumento só pode ser uma entre poucas palavras, descreva essas palavras como um enum e derive ValueEnum. Aí o clap aceita só essas palavras, lista todas elas na ajuda e recusa todo o resto. Estes enums substituem os campos String do Passo 4:
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}- Cada variante vira uma palavra em minúsculas:
Celsius→celsius,Km→km. #[value(alias = "c")]também aceita a forma curtac, sem mostrá-la na ajuda.- O clap precisa de
Clone. Também acrescentamosCopy, porque esses enums pequenos são baratos de copiar, e isso facilita omatch.
No enum Command, troque from: String por from: TempUnit (e por LengthUnit nos comprimentos). Também acrescentamos mais duas regras:
/// Quantas casas decimais mostrar#[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))]precision: u8,/// A temperatura a converter#[arg(allow_negative_numbers = true)]value: f64,value_parser!(u8).range(0..=6)só aceita uma precisão de 0 a 6. Mais do que isso só mostraria ruído.allow_negative_numbers = truedeixa você digitar-40. Sem isso, o clap acha que-40é uma opção curta chamada-4(veja Erros comuns). Temperaturas podem ser negativas, então permitimos; comprimentos não podem, então não permitimos.
Agora o clap pega as entradas ruins antes mesmo de o seu código rodar:
$ cargo run -q -- temp 20 --from rankine --to cerror: invalid value 'rankine' for '--from <FROM>' [possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.$ cargo run -q -- length 3 yd merror: invalid value 'yd' for '<FROM>' [possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.$ cargo run -q -- -p 9 length 1 m cmerror: invalid value '9' for '--precision <PRECISION>': 9 is not in 0..=6
For more information, try '--help'.O último erro diz que 9 não está no intervalo 0..=6 (9 is not in 0..=6). E a página de ajuda agora mostra os valores possíveis:
$ cargo run -q -- temp --helpConverte uma temperatura
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> A temperatura a converter
Options: -f, --from <FROM> A unidade de origem [possible values: celsius, fahrenheit, kelvin] -p, --precision <PRECISION> Quantas casas decimais mostrar [default: 2] -t, --to <TO> A unidade de destino [possible values: celsius, fahrenheit, kelvin] -h, --help Print helpEsta é a grande ideia do clap: depois que o parsing dá certo, os seus dados já são válidos. O from é um TempUnit de verdade, e não uma string que pode ser qualquer coisa. O resto do seu programa pode confiar nele.
Passo 6: Erros e códigos de saída
Seção intitulada “Passo 6: Erros e códigos de saída”Alguns erros o clap não consegue pegar, porque dependem do significado dos valores. -500 °C é um número perfeitamente válido, mas é mais frio que o zero absoluto (-273.15 °C), então não é uma temperatura real. Essa verificação fica no nosso próprio código.
Um padrão organizado é colocar o trabalho de verdade numa função run que devolve um Result, e deixar o main decidir o que mostrar e qual código de saída (exit code) devolver. Os programas informam sucesso ou falha ao shell com um número: 0 quer dizer sucesso, e qualquer outro valor quer dizer falha. O std::process::ExitCode do Rust deixa o main devolver um desses números:
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS // código de saída 0 } Err(message) => { eprintln!("erro: {message}"); // erros vão para o stderr ExitCode::FAILURE // código de saída 1 } }}O clap segue as mesmas regras. Quando o parsing falha, o Cli::parse() mostra o erro no stderr e sai com o código 2, o código de costume para “você usou o comando do jeito errado”. --help e --version saem com 0. Num terminal, echo $? mostra o último código de saída:
$ cargo run -q -- temp -500 -f c -t kerro: -500 °C é mais frio que o zero absoluto$ echo $?1$ cargo run -q -- length 3 yd merror: invalid value 'yd' for '<FROM>' [possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.$ echo $?2Repare que a primeira mensagem começa com erro:, porque fomos nós que escrevemos, e a segunda começa com error:, porque veio do clap.
Por que isso importa? Scripts e outros programas conferem o código de saída para saber se a sua ferramenta funcionou. A página Ambiente e processos explica códigos de saída e o stderr com mais detalhes. Em ferramentas maiores, você provavelmente usaria o anyhow como tipo de erro da run em vez de String, e ganharia contexto e cadeias de erros de graça.
Passo 7: O programa completo
Seção intitulada “Passo 7: O programa completo”Aqui está tudo junto. Substitua o src/main.rs por isto e experimente os comandos do começo da página.
use clap::{Parser, Subcommand, ValueEnum};use std::process::ExitCode;
/// Um pequeno conversor de unidades para temperaturas e comprimentos#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Quantas casas decimais mostrar #[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))] precision: u8,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Converte uma temperatura Temp { /// A temperatura a converter #[arg(allow_negative_numbers = true)] value: f64,
/// A unidade de origem #[arg(short, long)] from: TempUnit,
/// A unidade de destino #[arg(short, long)] to: TempUnit, }, /// Converte um comprimento Length { /// O comprimento a converter value: f64,
/// A unidade de origem from: LengthUnit,
/// A unidade de destino to: LengthUnit, },}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}
impl TempUnit { fn symbol(self) -> &'static str { match self { TempUnit::Celsius => "°C", TempUnit::Fahrenheit => "°F", TempUnit::Kelvin => "K", } }}
impl LengthUnit { // Quantos metros vale uma unidade desta. fn meters(self) -> f64 { match self { LengthUnit::Mm => 0.001, LengthUnit::Cm => 0.01, LengthUnit::M => 1.0, LengthUnit::Km => 1000.0, LengthUnit::In => 0.0254, LengthUnit::Ft => 0.3048, LengthUnit::Mi => 1609.344, } }
fn name(self) -> &'static str { match self { LengthUnit::Mm => "mm", LengthUnit::Cm => "cm", LengthUnit::M => "m", LengthUnit::Km => "km", LengthUnit::In => "in", LengthUnit::Ft => "ft", LengthUnit::Mi => "mi", } }}
fn convert_temp(value: f64, from: TempUnit, to: TempUnit) -> Result<f64, String> { // Passo 1: transformar o que recebemos em Celsius. let celsius = match from { TempUnit::Celsius => value, TempUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0, TempUnit::Kelvin => value - 273.15, }; if celsius < -273.15 { return Err(format!("{value} {} é mais frio que o zero absoluto", from.symbol())); } // Passo 2: transformar Celsius na unidade que queremos. Ok(match to { TempUnit::Celsius => celsius, TempUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0, TempUnit::Kelvin => celsius + 273.15, })}
fn convert_length(value: f64, from: LengthUnit, to: LengthUnit) -> f64 { value * from.meters() / to.meters()}
fn run(cli: &Cli) -> Result<String, String> { let p = cli.precision as usize; match cli.command { Command::Temp { value, from, to } => { let result = convert_temp(value, from, to)?; Ok(format!("{value} {} = {result:.p$} {}", from.symbol(), to.symbol())) } Command::Length { value, from, to } => { let result = convert_length(value, from, to); Ok(format!("{value} {} = {result:.p$} {}", from.name(), to.name())) } }}
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS } Err(message) => { eprintln!("erro: {message}"); ExitCode::FAILURE } }}Dois truques pequenos na run: {result:.p$} quer dizer “mostre o result com p casas decimais”, e o ? depois de convert_temp(...) sai mais cedo com o erro se a temperatura for impossível.
Passo 8: Testando o seu parser com try_parse_from
Seção intitulada “Passo 8: Testando o seu parser com try_parse_from”O Cli::parse() encerra o programa quando algo está errado. É isso que você quer para os usuários, mas não num teste. O Cli::try_parse_from(...) faz o mesmo parsing numa lista de argumentos que você escolhe e devolve um Result em vez de encerrar o programa. Isso faz dele perfeito para conferir o seu parser, e ele roda no Playground:
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
/// Um pequeno conversor de unidades para temperaturas e comprimentos#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Quantas casas decimais mostrar #[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))] precision: u8,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Converte uma temperatura Temp { /// A temperatura a converter #[arg(allow_negative_numbers = true)] value: f64,
/// A unidade de origem #[arg(short, long)] from: TempUnit,
/// A unidade de destino #[arg(short, long)] to: TempUnit, }, /// Converte um comprimento Length { /// O comprimento a converter value: f64,
/// A unidade de origem from: LengthUnit,
/// A unidade de destino to: LengthUnit, },}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum TempUnit { #[value(alias = "c")] Celsius, #[value(alias = "f")] Fahrenheit, #[value(alias = "k")] Kelvin,}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]enum LengthUnit { Mm, Cm, M, Km, In, Ft, Mi,}
fn check(line: &str) { println!("$ {line}"); // split_whitespace nos dá os argumentos, com o nome do programa primeiro. match Cli::try_parse_from(line.split_whitespace()) { Ok(cli) => println!(" ok: {cli:?}"), Err(err) => { // Mostra só a primeira linha da mensagem do clap. let first_line = err.to_string().lines().next().unwrap_or("").to_string(); println!(" {:?}: {first_line}", err.kind()); } }}
fn main() { // Entra em panic se o próprio parser estiver mal configurado (por exemplo, // duas opções com o mesmo nome curto). Um seguro barato. Cli::command().debug_assert();
check("convert temp -40 -f c -t fahrenheit"); check("convert length 5 km mi -p 1"); check("convert temp 20 --from rankine --to c"); check("convert -p 9 length 1 m cm"); check("convert temp 10 --to c"); check("convert --version");}Saída:
$ convert temp -40 -f c -t fahrenheit ok: Cli { precision: 2, command: Temp { value: -40.0, from: Celsius, to: Fahrenheit } }$ convert length 5 km mi -p 1 ok: Cli { precision: 1, command: Length { value: 5.0, from: Km, to: Mi } }$ convert temp 20 --from rankine --to c InvalidValue: error: invalid value 'rankine' for '--from <FROM>'$ convert -p 9 length 1 m cm ValueValidation: error: invalid value '9' for '--precision <PRECISION>': 9 is not in 0..=6$ convert temp 10 --to c MissingRequiredArgument: error: the following required arguments were not provided:$ convert --version DisplayVersion: convert 1.0Repare que o --version também volta como um Err. Para o clap, “mostrar a versão e parar” não é um parsing bem-sucedido, então ele é informado com o tipo DisplayVersion (e o --help com DisplayHelp). O Cli::parse() cuida desses casos para você e sai com o código 0.
No seu projeto de verdade, as mesmas verificações vão para os testes. Acrescente isto no fim do src/main.rs:
#[cfg(test)]mod tests { use super::*; use clap::CommandFactory;
#[test] fn cli_is_set_up_correctly() { Cli::command().debug_assert(); }
#[test] fn parses_a_temperature() { let cli = Cli::try_parse_from(["convert", "temp", "-40", "-f", "c", "-t", "f"]).unwrap(); assert_eq!(cli.precision, 2); match cli.command { Command::Temp { value, from, to } => { assert_eq!(value, -40.0); assert_eq!(from, TempUnit::Celsius); assert_eq!(to, TempUnit::Fahrenheit); } other => panic!("esperava temp, mas veio {other:?}"), } }
#[test] fn rejects_an_unknown_unit() { let result = Cli::try_parse_from(["convert", "length", "3", "yd", "m"]); let err = result.unwrap_err(); assert_eq!(err.kind(), clap::error::ErrorKind::InvalidValue); }
#[test] fn minus_forty_is_the_same_in_both() { let f = convert_temp(-40.0, TempUnit::Celsius, TempUnit::Fahrenheit).unwrap(); assert_eq!(f, -40.0); }}$ cargo test -q
running 4 tests....test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sCada ponto (.) é um teste que passou, e o resumo confirma: 4 aprovados (passed), 0 com falha (failed).
Erros comuns
Seção intitulada “Erros comuns”Esquecer a feature derive. Se você escrever cargo add clap sem --features derive, as macros de derive não existem:
error: cannot find derive macro `Parser` in this scope --> src/main.rs:4:10 |4 | #[derive(Parser, Debug)] | ^^^^^^ |note: `Parser` is imported here, but it is only a trait, without a derive macro --> src/main.rs:1:5 |1 | use clap::Parser; | ^^^^^^^^^^^^
error: cannot find attribute `command` in this scopeO compilador diz que não encontrou a macro de derive Parser e explica que, sem a feature, Parser é só um trait. Solução: rode cargo add clap --features derive, ou acrescente features = ["derive"] no Cargo.toml à mão.
Números negativos parecem opções. Com o programa do Passo 2, experimente um dia frio:
$ cargo run -q -- -40error: unexpected argument '-4' found
tip: to pass '-4' as a value, use '-- -4'
Usage: convert [OPTIONS] <CELSIUS>
For more information, try '--help'.O clap vê o - e acha que -4 é uma opção curta, por isso reclama de um argumento inesperado (unexpected argument). Solução: acrescente #[arg(allow_negative_numbers = true)] ao campo, como fizemos no Passo 5. (Os usuários também podem digitar -- -40; tudo o que vem depois de um -- sozinho é tratado como valor simples, como a dica tip sugere.)
Chamar Cli::parse() no Playground. O Playground roda o seu programa sem nenhum argumento, então o parse() mostra um erro (ou, numa ferramenta com subcomandos, a página de ajuda) e sai com o código 2 antes de o seu código fazer qualquer coisa. Lá, e nos testes, use Cli::parse_from([...]) ou Cli::try_parse_from([...]).
Mostrar erros com println!. Mensagens de erro vão no stderr (eprintln!), não no stdout. Se alguém rodar convert ... > resultado.txt, os erros continuam aparecendo na tela, em vez de ficarem escondidos dentro do arquivo.
Ideias para ir além
Seção intitulada “Ideias para ir além”- Mais unidades. Acrescente jardas (
yd) e milhas náuticas aoLengthUnit. O compilador vai apontar cadamatchque você precisa atualizar. - Um subcomando
weightpara quilos, libras e onças. - Uma flag
--jsonque mostra o resultado em JSON usando o serde_json. - Ler valores do stdin quando nenhum valor for informado, para você poder mandar números por um pipe:
echo 100 | convert temp -f c -t f. Um campoOption<f64>diz se um valor foi digitado. - Erros melhores com o anyhow. Mude a
runpara devolveranyhow::Result<String>e useanyhow::bail!. Veja anyhow e thiserror. - Autocompletar no shell. O crate clap_complete (em inglês) gera, a partir da sua struct
Cli, scripts de autocompletar com Tab para bash, zsh, fish e PowerShell.
- Adicione o clap com
cargo add clap --features derivee depois coloque#[derive(Parser)]numa struct. Cada campo é um argumento. - Campos simples são posicionais,
#[arg(short, long)]cria opções,boolcria flags edefault_value_tdefine valores padrão. Os comentários de documentação///viram o texto do--help. #[derive(Subcommand)]num enum dá subcomandos;#[derive(ValueEnum)]limita um argumento a um conjunto fixo de palavras.value_parser!(...).range(...)confere intervalos de números.- Depois que o parsing dá certo, os dados já são válidos. Coloque as verificações que sobram numa função
runque devolveResult, e devolva umExitCodenomain. - O clap sai com o código 2 em erros de uso e com 0 em
--help/--version. As suas próprias falhas também devem usar um código diferente de zero e mostrar a mensagem no stderr. Cli::try_parse_fromfaz o parsing de uma lista que você escolhe e devolve umResult: perfeito para testes e para o Playground.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Qual comando cargo add ativa o #[derive(Parser)]?
2. De onde o clap tira a descrição de cada opção no --help?
3. O que Cli::try_parse_from(["convert", "--version"]) devolve?
4. Um usuário digita uma unidade inválida e o Cli::parse() falha. Qual código de saída o clap usa?
5. Por que convert -40 falha no programa do Passo 2?
Pontuação: 0 / 5
Para saber mais
Seção intitulada “Para saber mais”Todos os links abaixo estão em inglês.
- clap no docs.rs: a documentação completa da API, com exemplos para cada atributo.
- The clap derive tutorial: o tutorial oficial, passo a passo, sobre argumentos, opções, subcomandos e validação.
- The clap derive reference: todas as opções de
#[command(...)],#[arg(...)]e#[value(...)], explicadas. - Command Line Applications in Rust: um livro gratuito sobre como criar ferramentas de linha de comando de verdade: saída, erros, testes e empacotamento.
- Exit codes — Command Line Applications in Rust: o que os códigos de saída significam e como usá-los bem.