Salta ai contenuti

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.

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.

Terminal window
$ cargo run -q -- temp 100 --from celsius --to fahrenheit
100 °C = 212.00 °F
$ cargo run -q -- temp -40 -f c -t f
-40 °C = -40.00 °F
$ cargo run -q -- length 5 km mi
5 km = 3.11 mi
$ cargo run -q -- -p 0 length 1 mi ft
1 mi = 5280 ft
$ cargo run -q -- temp 20 --from rankine --to c
error: 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.

Terminal window
cargo new convert
cd convert
cargo add clap --features derive

La 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.)

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)");
}
}
Terminal window
$ cargo run -q -- 21.5
21.5 °C = 70.70 °F
$ cargo run -q -- 100 --precision 1 --verbose
100 °C = 212.0 °F
(used °F = °C × 9/5 + 32)

clap legge ogni campo e decide che tipo di argomento è:

  • celsius non 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 = 2 si usa quando non la scrivi.
  • Un campo bool diventa un flag: vale true se c’è --verbose (o -v), false se 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 °F
▶ Provalo tu

Prova 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:

Terminal window
$ cargo run -q -- --help
Convert 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 -- --version
convert 1.0

La 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:

Terminal window
$ cargo run -q --
error: the following required arguments were not provided:
<CELSIUS>
Usage: convert <CELSIUS>
For more information, try '--help'.
$ cargo run -q -- abc
error: 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.

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 -> mi
▶ Provalo tu

Alcuni dettagli:

  • #[command(subcommand)] sul campo command dice a clap «questo campo contiene il sottocomando scelto».
  • Il nome della variante Temp diventa il sottocomando temp (clap lo mette in minuscolo per te).
  • temp usa opzioni con nome (--from c --to f), mentre length usa argomenti posizionali (5 km mi). Sono stili comuni entrambi; così li vedi tutti e due.
  • global = true permette a --precision di comparire prima o dopo il sottocomando. Senza, -p 1 sarebbe accettato solo prima di length.

La pagina di aiuto ora elenca i sottocomandi, e ogni sottocomando ha il suo aiuto:

Terminal window
$ cargo run -q -- --help
A 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 --help
Convert 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 help

Però c’è un problema. Le unità sono semplici String, quindi clap accetta qualsiasi cosa:

Terminal window
$ cargo run -q -- temp 20 --from rankine --to c
Cli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }

Dovremmo controllare ogni unità a mano più avanti. Facciamolo fare a clap.

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 breve c, senza mostrarla nell’aiuto.
  • clap ha bisogno di Clone. Aggiungiamo anche Copy, perché questi piccoli enum costano poco da copiare e così il match è 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 = true ti permette di scrivere -40. Senza, clap pensa che -40 sia 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:

Terminal window
$ cargo run -q -- temp 20 --from rankine --to c
error: invalid value 'rankine' for '--from <FROM>'
[possible values: celsius, fahrenheit, kelvin]
For more information, try '--help'.
$ cargo run -q -- length 3 yd m
error: 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 cm
error: 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:

Terminal window
$ cargo run -q -- temp --help
Convert 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 help

Questa è 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.

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:

Terminal window
$ cargo run -q -- temp -500 -f c -t k
error: -500 °C is colder than absolute zero
$ echo $?
1
$ cargo run -q -- length 3 yd m
error: invalid value 'yd' for '<FROM>'
[possible values: mm, cm, m, km, in, ft, mi]
For more information, try '--help'.
$ echo $?
2

Perché è 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.

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.

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.0
▶ Provalo tu

Nota 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);
}
}
Terminal window
$ cargo test -q
running 4 tests
....
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

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 scope

Il 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:

Terminal window
$ cargo run -q -- -40
error: 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.

  • Più unità. Aggiungi le iarde (yd) e le miglia nautiche a LengthUnit. Il compilatore ti indicherà ogni match da aggiornare.
  • Un sottocomando weight per chilogrammi, libbre e once.
  • Un flag --json che 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 campo Option<f64> ti dice se è stato scritto un valore.
  • Errori migliori con anyhow. Fai restituire a run un anyhow::Result<String> e usa anyhow::bail!. Vedi anyhow e thiserror.
  • Completamento nella shell. Il crate clap_complete (in inglese) genera dalla tua struct Cli gli script di completamento con Tab per bash, zsh, fish e PowerShell.
  • 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, bool crea flag e default_value_t imposta 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 run che restituisce Result, e restituisci un ExitCode da main.
  • 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_from analizza una lista scelta da te e restituisce un Result: perfetto per i test e per il Playground.

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