Progetto: tool da riga di comando con clap in Rust
Nella pagina Argomenti da riga di comando hai letto gli argomenti a mano con std::env::args(). Funziona per uno o due argomenti. Ma gli strumenti veri hanno bisogno di opzioni come --precision 3, di forme brevi come -p 3, di una pagina --help, di buoni messaggi di errore e di sottocomandi come git commit e git push. Scrivere tutto questo a mano vuol dire tanto codice noioso e facile da sbagliare.
clap (Command Line Argument Parser, in inglese) è il crate che usa quasi ogni CLI in Rust per questo lavoro. Descrivi gli argomenti come una normale struct Rust, e clap scrive per te il parser, il testo di aiuto e i messaggi di errore. In questo progetto costruisci convert, un piccolo convertitore di unità con due sottocomandi: temp per le temperature e length per le lunghezze.
Cosa costruirai
Sezione intitolata “Cosa costruirai”Ecco lo strumento finito in un terminale. Ogni output di questa pagina viene da un’esecuzione vera del programma, quindi i testi del programma (aiuto, messaggi, commenti di documentazione) restano in inglese, così come li stampano clap e il nostro codice.
$ 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'.Tutto quello che sta dopo -- va al tuo programma invece che a Cargo, e -q (quiet, silenzioso) nasconde le righe «Compiling…» di Cargo. L’ultimo messaggio di errore, che dice che rankine non è un valore valido per --from ed elenca i valori possibili? Non l’abbiamo scritto noi. L’ha scritto clap.
Passo 1: crea il progetto e aggiungi clap
Sezione intitolata “Passo 1: crea il progetto e aggiungi clap”cargo new convertcd convertcargo add clap --features deriveLa feature derive è la parte importante. Attiva #[derive(Parser)], il modo comodo di usare clap. Il tuo Cargo.toml ora contiene:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }(Il tuo numero di versione potrebbe essere un po’ più recente. Va bene qualsiasi versione che inizia con 4. Guarda Cargo e crate se cargo add è una novità per te.)
Passo 2: il tuo primo parser
Sezione intitolata “Passo 2: il tuo primo parser”Iniziamo con uno strumento che converte solo da Celsius a Fahrenheit. Scrivi una struct con un campo per ogni argomento e ci metti sopra #[derive(Parser)]. Sostituisci src/main.rs con:
use clap::Parser;
/// Convert a temperature from Celsius to Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// The temperature in Celsius celsius: f64,
/// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2)] precision: usize,
/// Also print the formula that was used #[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!("(used °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(used °F = °C × 9/5 + 32)clap legge ogni campo e decide che tipo di argomento è:
celsiusnon ha opzioni#[arg(...)], quindi è un argomento posizionale: il primo valore semplice che scrivi. Siccome il suo tipo èf64, clap converte anche per te il testo"21.5"in un numero.#[arg(short, long)]crea un’opzione con un nome breve e un nome lungo presi dal campo:-p/--precision.default_value_t = 2si usa quando non la scrivi.- Un campo
booldiventa un flag: valetruese c’è--verbose(o-v),falsese manca. - Un campo
Option<T>(qui non usato) sarebbe un argomento che puoi anche omettere del tutto.
{:.*} in println! significa «usa l’argomento successivo come numero di cifre decimali». È così che --precision arriva fino all’output.
Cli::parse() legge la vera riga di comando. Il Rust Playground non ha una riga di comando, quindi per provare lì il parser usa Cli::parse_from con una lista di argomenti finti. Il primo elemento è sempre il nome del programma, proprio come in std::env::args():
use clap::Parser;
/// Convert a temperature from Celsius to Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// The temperature in Celsius celsius: f64,
/// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2)] precision: usize,
/// Also print the formula that was used #[arg(short, long)] verbose: bool,}
fn main() { // Facciamo finta che l'utente abbia scritto: 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);}Output:
Cli { celsius: 100.0, precision: 1, verbose: true }100 °C = 212.0 °FProva a cambiare gli argomenti finti: togli "--verbose", oppure sostituisci "--precision" con "-p".
Passo 3: testo di aiuto dai commenti di documentazione
Sezione intitolata “Passo 3: testo di aiuto dai commenti di documentazione”Hai notato le righe /// nella struct? Sono commenti di documentazione, e clap li trasforma nella pagina di aiuto. Il commento sopra la struct descrive tutto il programma; il commento sopra ogni campo descrive quell’argomento. Ottieni --help (e -h) senza scrivere nient’altro:
$ cargo run -q -- --helpConvert a temperature from Celsius to Fahrenheit
Usage: convert [OPTIONS] <CELSIUS>
Arguments: <CELSIUS> The temperature in Celsius
Options: -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -v, --verbose Also print the formula that was used -h, --help Print help -V, --version Print version$ cargo run -q -- --versionconvert 1.0La riga #[command(name = "convert", version = "1.0")] imposta il nome e la versione mostrati qui. Se scrivi solo #[command(version)], clap prende invece la versione dal tuo Cargo.toml. Se scrivi i commenti /// in italiano, anche la pagina di aiuto sarà in italiano; le parti generate da clap (Usage:, Options:, Print help) restano in inglese.
clap ti spiega anche gli errori:
$ 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'.Il primo dice che manca l’argomento obbligatorio <CELSIUS>, il secondo che abc non è un numero decimale valido. Un buon testo di aiuto è un regalo al te stesso del futuro. Scrivi i commenti di documentazione come se qualcuno vedesse il tuo strumento per la prima volta, perché un giorno quel qualcuno sarai tu.
Passo 4: sottocomandi
Sezione intitolata “Passo 4: sottocomandi”Ora supportiamo anche le lunghezze. Strumenti come git e cargo usano i sottocomandi (cargo build, cargo test) per raggruppare lavori diversi. In clap, un sottocomando è un enum con #[derive(Subcommand)]. Ogni variante è un sottocomando, e i suoi campi sono gli argomenti di quel sottocomando:
use clap::{Parser, Subcommand};
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[arg(short, long, default_value_t = 2, global = true)] precision: usize,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Convert a temperature Temp { /// The temperature to convert value: f64,
/// The unit to convert from #[arg(short, long)] from: String,
/// The unit to convert to #[arg(short, long)] to: String, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: String,
/// The unit to convert to 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:?}");
// Scopri quale sottocomando è stato usato con un normale match. match cli.command { Command::Temp { value, from, to } => println!("temp: {value} {from} -> {to}"), Command::Length { value, from, to } => println!("length: {value} {from} -> {to}"), }}Output:
Cli { precision: 2, command: Temp { value: 100.0, from: "c", to: "f" } }Cli { precision: 1, command: Length { value: 5.0, from: "km", to: "mi" } }length: 5 km -> miAlcuni dettagli:
#[command(subcommand)]sul campocommanddice a clap «questo campo contiene il sottocomando scelto».- Il nome della variante
Tempdiventa il sottocomandotemp(clap lo mette in minuscolo per te). tempusa opzioni con nome (--from c --to f), mentrelengthusa argomenti posizionali (5 km mi). Sono stili comuni entrambi; così li vedi tutti e due.global = truepermette a--precisiondi comparire prima o dopo il sottocomando. Senza,-p 1sarebbe accettato solo prima dilength.
La pagina di aiuto ora elenca i sottocomandi, e ogni sottocomando ha il suo aiuto:
$ cargo run -q -- --helpA small unit converter for temperatures and lengths
Usage: convert [OPTIONS] <COMMAND>
Commands: temp Convert a temperature length Convert a length help Print this message or the help of the given subcommand(s)
Options: -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -h, --help Print help -V, --version Print version$ cargo run -q -- temp --helpConvert a temperature
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> The temperature to convert
Options: -f, --from <FROM> The unit to convert from -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -t, --to <TO> The unit to convert to -h, --help Print helpPerò c’è un problema. Le unità sono semplici String, quindi clap accetta qualsiasi cosa:
$ cargo run -q -- temp 20 --from rankine --to cCli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }Dovremmo controllare ogni unità a mano più avanti. Facciamolo fare a clap.
Passo 5: enum e validazione con ValueEnum
Sezione intitolata “Passo 5: enum e validazione con ValueEnum”Quando un argomento può essere solo una tra poche parole, descrivi quelle parole come un enum e deriva ValueEnum. clap allora accetta solo quelle parole, le elenca nell’aiuto e rifiuta tutto il resto. Questi enum sostituiscono i campi String del 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,}- Ogni variante diventa una parola minuscola:
Celsius→celsius,Km→km. #[value(alias = "c")]accetta anche la forma brevec, senza mostrarla nell’aiuto.- clap ha bisogno di
Clone. Aggiungiamo ancheCopy, perché questi piccoli enum costano poco da copiare e così ilmatchè più semplice.
Nell’enum Command, cambia from: String in from: TempUnit (e LengthUnit per le lunghezze). Aggiungiamo anche altre due regole:
/// How many digits to show after the decimal point#[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))]precision: u8,/// The temperature to convert#[arg(allow_negative_numbers = true)]value: f64,value_parser!(u8).range(0..=6)accetta solo una precisione da 0 a 6. Di più stamperebbe solo rumore.allow_negative_numbers = trueti permette di scrivere-40. Senza, clap pensa che-40sia un’opzione breve chiamata-4(vedi Errori comuni). Le temperature possono essere negative, quindi lo permettiamo; le lunghezze no, quindi lì non lo facciamo.
Ora clap intercetta l’input sbagliato prima ancora che parta il tuo codice:
$ 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'.E la pagina di aiuto ora mostra i valori possibili:
$ cargo run -q -- temp --helpConvert a temperature
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> The temperature to convert
Options: -f, --from <FROM> The unit to convert from [possible values: celsius, fahrenheit, kelvin] -p, --precision <PRECISION> How many digits to show after the decimal point [default: 2] -t, --to <TO> The unit to convert to [possible values: celsius, fahrenheit, kelvin] -h, --help Print helpQuesta è la grande idea di clap: quando il parsing riesce, i tuoi dati sono già validi. from è un vero TempUnit, non una stringa che potrebbe essere qualsiasi cosa. Il resto del programma può fidarsi.
Passo 6: errori e codici di uscita
Sezione intitolata “Passo 6: errori e codici di uscita”Alcuni errori clap non li può intercettare, perché dipendono dal significato dei valori. -500 °C è un numero valido, ma è più freddo dello zero assoluto (-273.15 °C), quindi non è una temperatura reale. Quel controllo spetta al nostro codice.
Un pattern ordinato è mettere il lavoro vero in una funzione run che restituisce un Result, e lasciare a main la decisione su cosa stampare e quale codice di uscita (exit code) restituire. I programmi comunicano successo o fallimento alla shell con un numero: 0 significa successo, qualsiasi altro valore significa fallimento. std::process::ExitCode di Rust permette a main di restituirne uno:
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS // codice di uscita 0 } Err(message) => { eprintln!("error: {message}"); // gli errori vanno su stderr ExitCode::FAILURE // codice di uscita 1 } }}clap segue le stesse regole. Quando il parsing fallisce, Cli::parse() stampa l’errore su stderr ed esce con il codice 2, il codice abituale per «hai usato il comando nel modo sbagliato». --help e --version escono con 0. In un terminale, echo $? mostra l’ultimo codice di uscita:
$ cargo run -q -- temp -500 -f c -t kerror: -500 °C is colder than absolute zero$ 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 $?2Perché è importante? Script e altri programmi controllano i codici di uscita per sapere se il tuo strumento ha funzionato. La pagina Ambiente e processi spiega più nel dettaglio codici di uscita e stderr. In strumenti più grandi useresti probabilmente anyhow come tipo di errore di run al posto di String, così ottieni contesto e catene di errori gratis.
Passo 7: il programma completo
Sezione intitolata “Passo 7: il programma completo”Ecco tutto insieme. Sostituisci src/main.rs con questo e prova i comandi all’inizio della pagina.
use clap::{Parser, Subcommand, ValueEnum};use std::process::ExitCode;
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[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 { /// Convert a temperature Temp { /// The temperature to convert #[arg(allow_negative_numbers = true)] value: f64,
/// The unit to convert from #[arg(short, long)] from: TempUnit,
/// The unit to convert to #[arg(short, long)] to: TempUnit, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: LengthUnit,
/// The unit to convert to 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 { // Quanti metri vale una di queste unità. 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: trasforma quello che abbiamo ricevuto in 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} {} is colder than absolute zero", from.symbol())); } // Passo 2: trasforma i Celsius nell'unità che vogliamo. 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!("error: {message}"); ExitCode::FAILURE } }}Due piccoli trucchi in run: {result:.p$} significa «stampa result con p cifre decimali», e il ? dopo convert_temp(...) esce subito con l’errore se la temperatura è impossibile.
Passo 8: testare il parser con try_parse_from
Sezione intitolata “Passo 8: testare il parser con try_parse_from”Cli::parse() chiude il programma quando qualcosa non va. È quello che vuoi per gli utenti, ma non in un test. Cli::try_parse_from(...) fa lo stesso parsing su una lista di argomenti scelta da te, e restituisce un Result invece di uscire. Per questo è perfetto per controllare il parser, e gira nel Playground:
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
/// A small unit converter for temperatures and lengths#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// How many digits to show after the decimal point #[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 { /// Convert a temperature Temp { /// The temperature to convert #[arg(allow_negative_numbers = true)] value: f64,
/// The unit to convert from #[arg(short, long)] from: TempUnit,
/// The unit to convert to #[arg(short, long)] to: TempUnit, }, /// Convert a length Length { /// The length to convert value: f64,
/// The unit to convert from from: LengthUnit,
/// The unit to convert to 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 ci dà gli argomenti, con il nome del programma per primo. match Cli::try_parse_from(line.split_whitespace()) { Ok(cli) => println!(" ok: {cli:?}"), Err(err) => { // Mostra solo la prima riga del messaggio di clap. let first_line = err.to_string().lines().next().unwrap_or("").to_string(); println!(" {:?}: {first_line}", err.kind()); } }}
fn main() { // Va in panic se il parser stesso è configurato male (per esempio, due // opzioni con lo stesso nome breve). Un'assicurazione che costa poco. 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");}Output:
$ 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.0Nota che anche --version torna indietro come Err. Per clap, «mostra la versione e fermati» non è un parsing riuscito, quindi viene segnalato con il tipo DisplayVersion (e --help con DisplayHelp). Cli::parse() gestisce questi casi per te ed esce con il codice 0.
Nel tuo progetto vero, gli stessi controlli vanno nei test. Aggiungi questo in fondo a 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!("expected temp, got {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.00sErrori comuni
Sezione intitolata “Errori comuni”Dimenticare la feature derive. Se scrivi cargo add clap senza --features derive, le macro derive non esistono:
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 scopeIl compilatore non trova la macro derive Parser: Parser è importato, ma è solo un trait, senza macro derive. Soluzione: esegui cargo add clap --features derive, oppure aggiungi a mano features = ["derive"] in Cargo.toml.
I numeri negativi sembrano opzioni. Con il programma del Passo 2, prova una giornata fredda:
$ 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'.clap vede il - e pensa che -4 sia un’opzione breve. Soluzione: aggiungi #[arg(allow_negative_numbers = true)] al campo, come abbiamo fatto al Passo 5. (Gli utenti possono anche scrivere -- -40: tutto quello che viene dopo un -- isolato è trattato come un valore semplice.)
Chiamare Cli::parse() nel Playground. Il Playground esegue il programma senza argomenti, quindi parse() stampa un errore (o, per uno strumento con sottocomandi, la pagina di aiuto) ed esce con il codice 2 prima che il tuo codice faccia qualsiasi cosa. Lì, e nei test, usa Cli::parse_from([...]) o Cli::try_parse_from([...]).
Stampare gli errori con println!. I messaggi di errore vanno su stderr (eprintln!), non su stdout. Se qualcuno esegue convert ... > result.txt, gli errori compaiono comunque sullo schermo invece di nascondersi nel file.
Idee per ampliarlo
Sezione intitolata “Idee per ampliarlo”- Più unità. Aggiungi le iarde (
yd) e le miglia nautiche aLengthUnit. Il compilatore ti indicherà ognimatchda aggiornare. - Un sottocomando
weightper chilogrammi, libbre e once. - Un flag
--jsonche stampa il risultato in JSON usando serde_json. - Leggere i valori da stdin quando non viene dato un valore, così puoi passare i numeri con una pipe:
echo 100 | convert temp -f c -t f. Un campoOption<f64>ti dice se è stato scritto un valore. - Errori migliori con anyhow. Fai restituire a
rununanyhow::Result<String>e usaanyhow::bail!. Vedi anyhow e thiserror. - Completamento nella shell. Il crate clap_complete (in inglese) genera dalla tua struct
Cligli script di completamento con Tab per bash, zsh, fish e PowerShell.
Riepilogo
Sezione intitolata “Riepilogo”- Aggiungi clap con
cargo add clap --features derive, poi metti#[derive(Parser)]su una struct. Ogni campo è un argomento. - I campi semplici sono posizionali,
#[arg(short, long)]crea opzioni,boolcrea flag edefault_value_timposta i valori predefiniti. I commenti di documentazione///diventano il testo di--help. #[derive(Subcommand)]su un enum ti dà i sottocomandi;#[derive(ValueEnum)]limita un argomento a un insieme fisso di parole.value_parser!(...).range(...)controlla gli intervalli numerici.- Quando il parsing riesce, i dati sono già validi. Metti i controlli rimanenti in una funzione
runche restituisceResult, e restituisci unExitCodedamain. - clap esce con il codice 2 per gli errori d’uso e con 0 per
--help/--version. Anche i tuoi errori dovrebbero usare un codice diverso da zero, e stampare su stderr. Cli::try_parse_fromanalizza una lista scelta da te e restituisce unResult: perfetto per i test e per il Playground.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quale comando cargo add attiva #[derive(Parser)]?
2. Da dove prende clap la descrizione di ogni opzione in --help?
3. Cosa restituisce Cli::try_parse_from(["convert", "--version"])?
4. Un utente scrive un'unità non valida e Cli::parse() fallisce. Quale codice di uscita usa clap?
5. Perché convert -40 fallisce con il programma del Passo 2?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”- clap su docs.rs (in inglese): la documentazione completa dell’API, con esempi per ogni attributo.
- Il tutorial derive di clap (in inglese): un tutorial ufficiale passo per passo su argomenti, opzioni, sottocomandi e validazione.
- Il riferimento derive di clap (in inglese): ogni opzione di
#[command(...)],#[arg(...)]e#[value(...)]spiegata. - Command Line Applications in Rust (in inglese): un libro gratuito su come costruire veri strumenti CLI: output, errori, test e distribuzione.
- Exit codes — Command Line Applications in Rust (in inglese): cosa significano i codici di uscita e come usarli bene.