Proyecto: Herramienta de línea de comandos con clap en Rust
En la página de Argumentos de línea de comandos leíste los argumentos a mano con std::env::args(). Eso funciona con uno o dos argumentos. Pero las herramientas de verdad necesitan opciones como --precision 3, formas cortas como -p 3, una página de --help, buenos mensajes de error y subcomandos como git commit y git push. Escribir todo eso a mano es mucho código aburrido y fácil de equivocar.
clap (Command Line Argument Parser, un analizador de argumentos de línea de comandos) es el crate que usa casi cualquier CLI (programa de terminal) escrito en Rust para esto. Describes tus argumentos como un struct normal de Rust, y clap escribe por ti el parser, el texto de ayuda y los mensajes de error. En este proyecto vas a construir convert, un pequeño conversor de unidades con dos subcomandos: temp para temperaturas y length para longitudes.
Lo que vas a construir
Sección titulada «Lo que vas a construir»Así se ve la herramienta terminada en una terminal. Todas las salidas de esta página salen de ejecutar el programa de verdad.
$ 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'.Todo lo que va después de -- se le pasa a tu programa en lugar de a Cargo, y -q (quiet, silencioso) oculta las líneas “Compiling…” del propio Cargo. ¿Y ese último mensaje de error? No lo escribimos nosotros. Lo escribió clap. Sus mensajes están en inglés: aquí dice que rankine no es un valor válido para --from y te enseña los valores posibles.
Paso 1: Crea el proyecto y añade clap
Sección titulada «Paso 1: Crea el proyecto y añade clap»cargo new convertcd convertcargo add clap --features deriveLa parte importante es la feature derive. Activa #[derive(Parser)], que es la forma cómoda de usar clap. Tu Cargo.toml ahora contiene:
[dependencies]clap = { version = "4.6.7", features = ["derive"] }(Puede que tu número de versión sea un poco más nuevo. Cualquiera que empiece por 4 sirve para esta página. Si cargo add es nuevo para ti, mira Cargo y crates.)
Paso 2: Tu primer parser
Sección titulada «Paso 2: Tu primer parser»Empecemos con una herramienta que solo convierte de Celsius a Fahrenheit. Escribes un struct con un campo por cada argumento y le pones #[derive(Parser)]. Reemplaza src/main.rs por esto:
use clap::Parser;
/// Convierte una temperatura de Celsius a Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// La temperatura en grados Celsius celsius: f64,
/// Cuántos decimales mostrar #[arg(short, long, default_value_t = 2)] precision: usize,
/// Imprime también la fórmula 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)clap lee cada campo y decide qué tipo de argumento es:
celsiusno tiene opciones#[arg(...)], así que es un argumento posicional: el primer valor suelto que escribes. Como su tipo esf64, clap también convierte el texto"21.5"en un número por ti.#[arg(short, long)]crea una opción con un nombre corto y uno largo, sacados del nombre del campo:-p/--precision.default_value_t = 2es el valor que se usa cuando no la escribes.- Un campo
boolse convierte en un flag (indicador): valetruesi aparece--verbose(o-v) yfalsesi no. - Un campo
Option<T>(aquí no lo usamos) sería un argumento que puedes omitir por completo.
{:.*} en println! significa “usa el siguiente argumento como número de decimales”. Así es como --precision llega a la salida.
Cli::parse() lee la línea de comandos real. El Rust Playground no tiene línea de comandos, así que para probar el parser allí, usa Cli::parse_from con una lista de argumentos de mentira. El primer elemento siempre es el nombre del programa, igual que con std::env::args():
use clap::Parser;
/// Convierte una temperatura de Celsius a Fahrenheit#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// La temperatura en grados Celsius celsius: f64,
/// Cuántos decimales mostrar #[arg(short, long, default_value_t = 2)] precision: usize,
/// Imprime también la fórmula usada #[arg(short, long)] verbose: bool,}
fn main() { // Imagina que el usuario escribió: 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);}Salida:
Cli { celsius: 100.0, precision: 1, verbose: true }100 °C = 212.0 °FPrueba a cambiar los argumentos de mentira: quita "--verbose", o cambia "--precision" por "-p".
Paso 3: El texto de ayuda sale de los comentarios de documentación
Sección titulada «Paso 3: El texto de ayuda sale de los comentarios de documentación»¿Te fijaste en las líneas /// del struct? Son comentarios de documentación, y clap los convierte en la página de ayuda. El comentario encima del struct describe todo el programa, y el comentario encima de cada campo describe ese argumento. Obtienes --help (y -h) sin escribir nada más:
$ cargo run -q -- --helpConvierte una temperatura de Celsius a Fahrenheit
Usage: convert [OPTIONS] <CELSIUS>
Arguments: <CELSIUS> La temperatura en grados Celsius
Options: -p, --precision <PRECISION> Cuántos decimales mostrar [default: 2] -v, --verbose Imprime también la fórmula usada -h, --help Print help -V, --version Print version$ cargo run -q -- --versionconvert 1.0Las descripciones son tuyas, así que salen en español. Los títulos (Usage, Arguments, Options) y las líneas de --help y --version los pone clap, y por eso salen en inglés.
La línea #[command(name = "convert", version = "1.0")] define el nombre y la versión que se ven aquí. Si escribes solo #[command(version)], clap toma la versión de tu Cargo.toml.
clap también te explica los errores:
$ 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'.El primer error dice que falta el argumento obligatorio <CELSIUS>. El segundo, que abc no es un número decimal válido.
Un buen texto de ayuda es un regalo para tu yo del futuro. Escribe los comentarios de documentación como si alguien viera tu herramienta por primera vez, porque un día esa persona serás tú.
Paso 4: Subcomandos
Sección titulada «Paso 4: Subcomandos»Ahora vamos a admitir también longitudes. Herramientas como git y cargo usan subcomandos (cargo build, cargo test) para agrupar tareas distintas. En clap, un subcomando es un enum con #[derive(Subcommand)]. Cada variante es un subcomando, y sus campos son los argumentos de ese subcomando:
use clap::{Parser, Subcommand};
/// Un pequeño conversor de unidades de temperatura y longitud#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Cuántos decimales mostrar #[arg(short, long, default_value_t = 2, global = true)] precision: usize,
#[command(subcommand)] command: Command,}
#[derive(Subcommand, Debug)]enum Command { /// Convierte una temperatura Temp { /// La temperatura que quieres convertir value: f64,
/// La unidad de origen #[arg(short, long)] from: String,
/// La unidad de destino #[arg(short, long)] to: String, }, /// Convierte una longitud Length { /// La longitud que quieres convertir value: f64,
/// La unidad de origen from: String,
/// La unidad 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:?}");
// Averiguas qué subcomando se usó con un match normal. match cli.command { Command::Temp { value, from, to } => println!("temp: {value} {from} -> {to}"), Command::Length { value, from, to } => println!("length: {value} {from} -> {to}"), }}Salida:
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 -> miAlgunos detalles:
#[command(subcommand)]en el campocommandle dice a clap “este campo guarda el subcomando elegido”.- El nombre de la variante
Tempse convierte en el subcomandotemp(clap lo pasa a minúsculas por ti). tempusa opciones con nombre (--from c --to f), mientras quelengthusa argumentos posicionales (5 km mi). Los dos estilos son habituales, y así ves cada uno.global = truepermite que--precisionaparezca antes o después del subcomando. Sin eso,-p 1solo se aceptaría antes delength.
La página de ayuda ahora enumera los subcomandos, y cada subcomando tiene su propia ayuda:
$ cargo run -q -- --helpUn pequeño conversor de unidades de temperatura y longitud
Usage: convert [OPTIONS] <COMMAND>
Commands: temp Convierte una temperatura length Convierte una longitud help Print this message or the help of the given subcommand(s)
Options: -p, --precision <PRECISION> Cuántos decimales mostrar [default: 2] -h, --help Print help -V, --version Print version$ cargo run -q -- temp --helpConvierte una temperatura
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> La temperatura que quieres convertir
Options: -f, --from <FROM> La unidad de origen -p, --precision <PRECISION> Cuántos decimales mostrar [default: 2] -t, --to <TO> La unidad de destino -h, --help Print helpPero hay un problema. Las unidades son String normales, así que clap acepta cualquier cosa:
$ cargo run -q -- temp 20 --from rankine --to cCli { precision: 2, command: Temp { value: 20.0, from: "rankine", to: "c" } }Tendríamos que comprobar cada unidad a mano más adelante. Mejor que lo haga clap.
Paso 5: Enums y validación con ValueEnum
Sección titulada «Paso 5: Enums y validación con ValueEnum»Cuando un argumento solo puede ser una de unas pocas palabras, describe esas palabras como un enum y deriva ValueEnum. Así clap acepta solo esas palabras, las enumera en la ayuda y rechaza todo lo demás. Estos enums sustituyen a los campos String del paso 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 se convierte en una palabra en minúsculas:
Celsius→celsius,Km→km. #[value(alias = "c")]acepta también la forma cortac, sin mostrarla en la ayuda.- clap necesita
Clone. Añadimos tambiénCopy, porque estos enums pequeños son baratos de copiar y eso facilita losmatch.
En el enum Command, cambia from: String por from: TempUnit (y por LengthUnit en las longitudes). Además, añadimos dos reglas más:
/// Cuántos decimales mostrar#[arg(short, long, default_value_t = 2, global = true, value_parser = clap::value_parser!(u8).range(0..=6))]precision: u8,/// La temperatura que quieres convertir#[arg(allow_negative_numbers = true)]value: f64,value_parser!(u8).range(0..=6)solo acepta una precisión de 0 a 6. Más decimales solo imprimirían ruido.allow_negative_numbers = truete deja escribir-40. Sin eso, clap cree que-40es una opción corta llamada-4(mira Errores comunes). Las temperaturas pueden ser negativas, así que lo permitimos; las longitudes no, así que no.
Ahora clap detecta la entrada incorrecta antes de que tu código llegue a ejecutarse:
$ 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'.Y la página de ayuda ahora muestra los valores posibles (possible values):
$ cargo run -q -- temp --helpConvierte una temperatura
Usage: convert temp [OPTIONS] --from <FROM> --to <TO> <VALUE>
Arguments: <VALUE> La temperatura que quieres convertir
Options: -f, --from <FROM> La unidad de origen [possible values: celsius, fahrenheit, kelvin] -p, --precision <PRECISION> Cuántos decimales mostrar [default: 2] -t, --to <TO> La unidad de destino [possible values: celsius, fahrenheit, kelvin] -h, --help Print helpEsta es la gran idea de clap: una vez que el análisis sale bien, tus datos ya son válidos. from es un TempUnit de verdad, no una cadena de texto que podría ser cualquier cosa. El resto de tu programa puede fiarse de él.
Paso 6: Errores y códigos de salida
Sección titulada «Paso 6: Errores y códigos de salida»Hay errores que clap no puede detectar, porque dependen del significado de los valores. -500 °C es un número perfectamente válido, pero está por debajo del cero absoluto (-273.15 °C), así que no es una temperatura real. Esa comprobación le toca a nuestro propio código.
Un patrón ordenado es poner el trabajo de verdad en una función run que devuelve un Result, y dejar que main decida qué imprimir y qué código de salida (exit code) devolver. Los programas le informan del éxito o del fallo a la shell con un número: 0 significa éxito, y cualquier otro número significa fallo. std::process::ExitCode de Rust permite que main devuelva uno:
fn main() -> ExitCode { let cli = Cli::parse(); match run(&cli) { Ok(line) => { println!("{line}"); ExitCode::SUCCESS // código de salida 0 } Err(message) => { eprintln!("error: {message}"); // los errores van a stderr ExitCode::FAILURE // código de salida 1 } }}clap sigue las mismas reglas. Cuando el análisis falla, Cli::parse() imprime el error en stderr y termina con el código 2, el habitual para “usaste mal el comando”. --help y --version terminan con 0. En una terminal, echo $? muestra el último código de salida:
$ cargo run -q -- temp -500 -f c -t kerror: -500 °C está por debajo del cero 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 $?2¿Por qué importa esto? Los scripts y otros programas miran el código de salida para saber si tu herramienta funcionó. La página de Variables de entorno y procesos explica con más detalle los códigos de salida y stderr. En herramientas más grandes, seguramente usarías anyhow como tipo de error de run en lugar de String, y así tendrías contexto y cadenas de errores gratis.
Paso 7: El programa completo
Sección titulada «Paso 7: El programa completo»Aquí lo tienes todo junto. Reemplaza src/main.rs por esto y prueba los comandos del principio de la página.
use clap::{Parser, Subcommand, ValueEnum};use std::process::ExitCode;
/// Un pequeño conversor de unidades de temperatura y longitud#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Cuántos decimales 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 { /// Convierte una temperatura Temp { /// La temperatura que quieres convertir #[arg(allow_negative_numbers = true)] value: f64,
/// La unidad de origen #[arg(short, long)] from: TempUnit,
/// La unidad de destino #[arg(short, long)] to: TempUnit, }, /// Convierte una longitud Length { /// La longitud que quieres convertir value: f64,
/// La unidad de origen from: LengthUnit,
/// La unidad 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 { // Cuántos metros mide una de estas unidades. 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> { // Paso 1: convierte lo que nos llegó a 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} {} está por debajo del cero absoluto", from.symbol())); } // Paso 2: convierte Celsius a la unidad 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!("error: {message}"); ExitCode::FAILURE } }}Dos pequeños trucos en run: {result:.p$} significa “imprime result con p decimales”, y el ? después de convert_temp(...) sale antes con el error si la temperatura es imposible.
Paso 8: Prueba tu parser con try_parse_from
Sección titulada «Paso 8: Prueba tu parser con try_parse_from»Cli::parse() termina el programa cuando algo va mal. Eso es lo que quieres para los usuarios, pero no en una prueba. Cli::try_parse_from(...) hace el mismo análisis sobre una lista de argumentos que eliges tú, y devuelve un Result en lugar de terminar el programa. Eso lo hace perfecto para comprobar tu parser, y además funciona en el Playground:
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
/// Un pequeño conversor de unidades de temperatura y longitud#[derive(Parser, Debug)]#[command(name = "convert", version = "1.0")]struct Cli { /// Cuántos decimales 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 { /// Convierte una temperatura Temp { /// La temperatura que quieres convertir #[arg(allow_negative_numbers = true)] value: f64,
/// La unidad de origen #[arg(short, long)] from: TempUnit,
/// La unidad de destino #[arg(short, long)] to: TempUnit, }, /// Convierte una longitud Length { /// La longitud que quieres convertir value: f64,
/// La unidad de origen from: LengthUnit,
/// La unidad 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 da los argumentos, con el nombre del programa primero. match Cli::try_parse_from(line.split_whitespace()) { Ok(cli) => println!(" ok: {cli:?}"), Err(err) => { // Muestra solo la primera línea del mensaje de clap. let first_line = err.to_string().lines().next().unwrap_or("").to_string(); println!(" {:?}: {first_line}", err.kind()); } }}
fn main() { // Hace panic si el propio parser está mal configurado (por ejemplo, dos // opciones con el mismo nombre corto). Un seguro que cuesta 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");}Salida:
$ 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.0Fíjate en que --version también vuelve como un Err. Para clap, “muestra la versión y para” no es un análisis exitoso, así que lo informa con el tipo DisplayVersion (y --help con DisplayHelp). Cli::parse() los maneja por ti y termina con el código 0.
En tu proyecto real, estas mismas comprobaciones van en pruebas. Añade esto al final de 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!("se esperaba temp, se obtuvo {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.00sErrores comunes
Sección titulada «Errores comunes»Olvidar la feature derive. Si escribes cargo add clap sin --features derive, las macros derive no existen:
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 scopeEl compilador dice que no encuentra la macro derive Parser: lo que importaste es solo el trait, sin la macro. La solución: ejecuta cargo add clap --features derive, o añade features = ["derive"] a mano en Cargo.toml.
Los números negativos parecen opciones. Con el programa del paso 2, prueba con un día de mucho frío:
$ 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 ve el - y cree que -4 es una opción corta. La solución: añade #[arg(allow_negative_numbers = true)] al campo, como hicimos en el paso 5. (Los usuarios también pueden escribir -- -40: todo lo que va después de un -- suelto se trata como un valor normal.)
Llamar a Cli::parse() en el Playground. El Playground ejecuta tu programa sin argumentos, así que parse() imprime un error (o, en una herramienta con subcomandos, la página de ayuda) y termina con el código 2 antes de que tu código haga nada. Allí, y en las pruebas, usa Cli::parse_from([...]) o Cli::try_parse_from([...]).
Imprimir los errores con println!. Los mensajes de error van en stderr (eprintln!), no en stdout. Si alguien ejecuta convert ... > result.txt, los errores siguen apareciendo en pantalla en lugar de esconderse dentro del archivo.
Ideas para ampliarlo
Sección titulada «Ideas para ampliarlo»- Más unidades. Añade yardas (
yd) y millas náuticas aLengthUnit. El compilador te señalará cadamatchque tengas que actualizar. - Un subcomando
weightpara kilos, libras y onzas. - Un flag
--jsonque imprima el resultado como JSON usando serde_json. - Leer valores de stdin cuando no se da ningún valor, para poder pasarle números con una tubería:
echo 100 | convert temp -f c -t f. Un campoOption<f64>te dice si se escribió un valor. - Mejores errores con anyhow. Cambia
runpara que devuelvaanyhow::Result<String>y usaanyhow::bail!. Mira anyhow y thiserror. - Autocompletado en la shell. El crate clap_complete genera, a partir de tu struct
Cli, scripts de autocompletado con tabulador para bash, zsh, fish y PowerShell.
Resumen
Sección titulada «Resumen»- Añade clap con
cargo add clap --features derivey luego pon#[derive(Parser)]en un struct. Cada campo es un argumento. - Los campos sin más son posicionales,
#[arg(short, long)]crea opciones,boolcrea flags ydefault_value_tdefine valores por defecto. Los comentarios de documentación///se convierten en el texto de--help. #[derive(Subcommand)]en un enum te da subcomandos;#[derive(ValueEnum)]limita un argumento a un conjunto fijo de palabras.value_parser!(...).range(...)comprueba rangos de números.- Cuando el análisis sale bien, los datos ya son válidos. Pon las comprobaciones que falten en una función
runque devuelvaResult, y devuelve unExitCodedesdemain. - clap termina con el código 2 en los errores de uso y con 0 en
--help/--version. Tus propios fallos también deberían usar un código distinto de cero e imprimirse en stderr. Cli::try_parse_fromanaliza una lista que eliges tú y devuelve unResult: perfecto para las pruebas y para el Playground.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué comando cargo add activa #[derive(Parser)]?
2. ¿De dónde saca clap la descripción de cada opción en --help?
3. ¿Qué devuelve Cli::try_parse_from(["convert", "--version"])?
4. Un usuario escribe una unidad no válida y Cli::parse() falla. ¿Qué código de salida usa clap?
5. ¿Por qué falla convert -40 en el programa del paso 2?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- clap en docs.rs (en inglés): la documentación completa de la API, con ejemplos para cada atributo.
- El tutorial de clap con derive (en inglés): un tutorial oficial paso a paso sobre argumentos, opciones, subcomandos y validación.
- La referencia de clap con derive (en inglés): todas las opciones de
#[command(...)],#[arg(...)]y#[value(...)], explicadas. - Command Line Applications in Rust (en inglés): un libro gratuito sobre cómo crear herramientas de terminal de verdad: salida, errores, pruebas y empaquetado.
- Exit codes — Command Line Applications in Rust (en inglés): qué significan los códigos de salida y cómo usarlos bien.