diff --git a/clap_derive/examples/arg_enum.rs b/clap_derive/examples/arg_enum.rs index e233b053b4a..e5ca96230d3 100644 --- a/clap_derive/examples/arg_enum.rs +++ b/clap_derive/examples/arg_enum.rs @@ -4,13 +4,19 @@ use clap::{ArgEnum, Clap}; -#[derive(ArgEnum, Debug, PartialEq)] +#[derive(ArgEnum, Debug, PartialEq, Clone)] enum ArgChoice { + /// Descriptions are supported as doc-comment Foo, + // Renames are supported + #[clap(name = "b-a-r")] Bar, // Aliases are supported #[clap(alias = "b", alias = "z")] Baz, + // Hiding variants from help and completion is supported + #[clap(hidden = true)] + Hidden, } #[derive(Clap, PartialEq, Debug)] diff --git a/clap_derive/examples/value_hints_derive.rs b/clap_derive/examples/value_hints_derive.rs index 0ecca5fb325..d0be2c00e82 100644 --- a/clap_derive/examples/value_hints_derive.rs +++ b/clap_derive/examples/value_hints_derive.rs @@ -19,7 +19,7 @@ use std::ffi::OsString; use std::io; use std::path::PathBuf; -#[derive(ArgEnum, Debug, PartialEq)] +#[derive(ArgEnum, Debug, PartialEq, Clone)] enum GeneratorChoice { Bash, Elvish, diff --git a/clap_derive/src/attrs.rs b/clap_derive/src/attrs.rs index 0d465f817ca..85b86154e06 100644 --- a/clap_derive/src/attrs.rs +++ b/clap_derive/src/attrs.rs @@ -819,14 +819,6 @@ impl Attrs { } } - pub fn enum_aliases(&self) -> Vec { - self.methods - .iter() - .filter(|m| m.name == "alias") - .map(|m| m.args.clone()) - .collect() - } - pub fn casing(&self) -> Sp { self.casing.clone() } diff --git a/clap_derive/src/derives/arg_enum.rs b/clap_derive/src/derives/arg_enum.rs index 9bae00725b2..8d6a878f095 100644 --- a/clap_derive/src/derives/arg_enum.rs +++ b/clap_derive/src/derives/arg_enum.rs @@ -47,9 +47,8 @@ pub fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStr ); let lits = lits(&e.variants, &attrs); - let variants = gen_variants(&lits); - let from_str = gen_from_str(&lits); - let as_arg = gen_as_arg(&lits); + let arg_values = gen_arg_values(&lits); + let arg_value = gen_arg_value(&lits); quote! { #[allow(dead_code, unreachable_code, unused_variables)] @@ -65,9 +64,8 @@ pub fn gen_for_enum(name: &Ident, attrs: &[Attribute], e: &DataEnum) -> TokenStr )] #[deny(clippy::correctness)] impl clap::ArgEnum for #name { - #variants - #from_str - #as_arg + #arg_values + #arg_value } } } @@ -87,54 +85,35 @@ fn lits( if let Kind::Skip(_) = &*attrs.kind() { None } else { - Some((variant, attrs)) + let fields = attrs.field_methods(); + let name = attrs.cased_name(); + Some(( + quote! { + clap::ArgValue::new(#name) + #fields + }, + variant.ident.clone(), + )) } }) - .flat_map(|(variant, attrs)| { - let mut ret = vec![(attrs.cased_name(), variant.ident.clone())]; - - attrs - .enum_aliases() - .into_iter() - .for_each(|x| ret.push((x, variant.ident.clone()))); - - ret - }) .collect::>() } -fn gen_variants(lits: &[(TokenStream, Ident)]) -> TokenStream { - let lit = lits.iter().map(|l| &l.0).collect::>(); - - quote! { - const VARIANTS: &'static [&'static str] = &[#(#lit),*]; - } -} - -fn gen_from_str(lits: &[(TokenStream, Ident)]) -> TokenStream { - let (lit, variant): (Vec, Vec) = lits.iter().cloned().unzip(); +fn gen_arg_values(lits: &[(TokenStream, Ident)]) -> TokenStream { + let lit = lits.iter().map(|l| &l.1).collect::>(); quote! { - fn from_str(input: &str, case_insensitive: bool) -> ::std::result::Result { - let func = if case_insensitive { - ::std::ascii::AsciiExt::eq_ignore_ascii_case - } else { - str::eq - }; - - match input { - #(val if func(val, #lit) => Ok(Self::#variant),)* - e => Err(format!("Invalid variant: {}", e)), - } + fn value_variants<'a>() -> &'a [Self]{ + &[#(Self::#lit),*] } } } -fn gen_as_arg(lits: &[(TokenStream, Ident)]) -> TokenStream { +fn gen_arg_value(lits: &[(TokenStream, Ident)]) -> TokenStream { let (lit, variant): (Vec, Vec) = lits.iter().cloned().unzip(); quote! { - fn as_arg(&self) -> Option<&'static str> { + fn arg_value<'a>(&self) -> Option> { match self { #(Self::#variant => Some(#lit),)* _ => None diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs index 398489def06..ec7b6fdbb2b 100644 --- a/clap_derive/src/derives/args.rs +++ b/clap_derive/src/derives/args.rs @@ -361,7 +361,7 @@ pub fn gen_augment( fn gen_arg_enum_possible_values(ty: &Type) -> TokenStream { quote_spanned! { ty.span()=> - .possible_values(<#ty as clap::ArgEnum>::VARIANTS.into_iter().copied()) + .possible_values(<#ty as clap::ArgEnum>::value_variants().iter().filter_map(clap::ArgEnum::arg_value)) } } diff --git a/clap_derive/src/dummies.rs b/clap_derive/src/dummies.rs index 433fff3f16e..0f04567e770 100644 --- a/clap_derive/src/dummies.rs +++ b/clap_derive/src/dummies.rs @@ -76,11 +76,13 @@ pub fn args(name: &Ident) { pub fn arg_enum(name: &Ident) { append_dummy(quote! { impl clap::ArgEnum for #name { - const VARIANTS: &'static [&'static str] = &[]; + fn value_variants<'a>() -> &'a [Self]{ + unimplemented!() + } fn from_str(_input: &str, _case_insensitive: bool) -> Result { unimplemented!() } - fn as_arg(&self) -> Option<&'static str> { + fn arg_value<'a>(&self) -> Option>{ unimplemented!() } } diff --git a/clap_derive/tests/arg_enum.rs b/clap_derive/tests/arg_enum.rs index cbffaa41684..f68cc70ce5d 100644 --- a/clap_derive/tests/arg_enum.rs +++ b/clap_derive/tests/arg_enum.rs @@ -7,11 +7,11 @@ // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. -use clap::{ArgEnum, Clap}; +use clap::{ArgEnum, ArgValue, Clap}; #[test] fn basic() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, Bar, @@ -40,7 +40,7 @@ fn basic() { #[test] fn default_value() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, Bar, @@ -54,7 +54,7 @@ fn default_value() { impl std::fmt::Display for ArgChoice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - std::fmt::Display::fmt(self.as_arg().unwrap(), f) + std::fmt::Display::fmt(self.arg_value().unwrap().get_name(), f) } } @@ -86,7 +86,7 @@ fn default_value() { #[test] fn multi_word_is_renamed_kebab() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] #[allow(non_camel_case_types)] enum ArgChoice { FooBar, @@ -116,7 +116,7 @@ fn multi_word_is_renamed_kebab() { #[test] fn variant_with_defined_casing() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { #[clap(rename_all = "screaming_snake")] FooBar, @@ -139,7 +139,7 @@ fn variant_with_defined_casing() { #[test] fn casing_is_propogated_from_parent() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] #[clap(rename_all = "screaming_snake")] enum ArgChoice { FooBar, @@ -162,7 +162,7 @@ fn casing_is_propogated_from_parent() { #[test] fn casing_propogation_is_overridden() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] #[clap(rename_all = "screaming_snake")] enum ArgChoice { #[clap(rename_all = "camel")] @@ -187,7 +187,7 @@ fn casing_propogation_is_overridden() { #[test] fn case_insensitive() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, } @@ -214,7 +214,7 @@ fn case_insensitive() { #[test] fn case_insensitive_set_to_false() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, } @@ -236,7 +236,7 @@ fn case_insensitive_set_to_false() { #[test] fn alias() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { #[clap(alias = "TOTP")] TOTP, @@ -264,7 +264,7 @@ fn alias() { #[test] fn multiple_alias() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { #[clap(alias = "TOTP", alias = "t")] TOTP, @@ -298,7 +298,7 @@ fn multiple_alias() { #[test] fn option() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, Bar, @@ -328,7 +328,7 @@ fn option() { #[test] fn vector() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, Bar, @@ -358,7 +358,7 @@ fn vector() { #[test] fn skip_variant() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] #[allow(dead_code)] // silence warning about `Baz` being unused enum ArgChoice { Foo, @@ -367,7 +367,14 @@ fn skip_variant() { Baz, } - assert_eq!(ArgChoice::VARIANTS, ["foo", "bar"]); + assert_eq!( + ArgChoice::value_variants() + .iter() + .map(ArgEnum::arg_value) + .map(Option::unwrap) + .collect::>(), + vec![ArgValue::new("foo"), ArgValue::new("bar")] + ); assert!(ArgChoice::from_str("foo", true).is_ok()); assert!(ArgChoice::from_str("bar", true).is_ok()); assert!(ArgChoice::from_str("baz", true).is_err()); @@ -375,7 +382,7 @@ fn skip_variant() { #[test] fn from_str_invalid() { - #[derive(ArgEnum, PartialEq, Debug)] + #[derive(ArgEnum, PartialEq, Debug, Clone)] enum ArgChoice { Foo, } diff --git a/clap_derive/tests/ui/arg_enum_on_struct.rs b/clap_derive/tests/ui/arg_enum_on_struct.rs index cb41f72b2f7..a26c9658aac 100644 --- a/clap_derive/tests/ui/arg_enum_on_struct.rs +++ b/clap_derive/tests/ui/arg_enum_on_struct.rs @@ -1,8 +1,8 @@ use clap::ArgEnum; -#[derive(ArgEnum, Debug)] +#[derive(ArgEnum, Clone, Debug)] struct Opt {} fn main() { - println!("{:?}", Opt::VARIANTS); + println!("{:?}", Opt::value_variants()); } diff --git a/clap_derive/tests/ui/arg_enum_on_struct.stderr b/clap_derive/tests/ui/arg_enum_on_struct.stderr index d83386a4118..3b7175d7c93 100644 --- a/clap_derive/tests/ui/arg_enum_on_struct.stderr +++ b/clap_derive/tests/ui/arg_enum_on_struct.stderr @@ -1,7 +1,7 @@ error: `#[derive(ArgEnum)]` only supports enums --> $DIR/arg_enum_on_struct.rs:3:10 | -3 | #[derive(ArgEnum, Debug)] +3 | #[derive(ArgEnum, Clone, Debug)] | ^^^^^^^ | = note: this error originates in the derive macro `ArgEnum` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/src/build/arg/arg_value.rs b/src/build/arg/arg_value.rs index 83de81b35d1..af655579fda 100644 --- a/src/build/arg/arg_value.rs +++ b/src/build/arg/arg_value.rs @@ -1,3 +1,5 @@ +use std::iter; + /// The representation of a possible value of an argument. /// /// This is used for specifying [possible values] of [Args]. @@ -20,10 +22,11 @@ /// [possible values]: crate::Arg::possible_value() /// [hide]: ArgValue::hidden() /// [about]: ArgValue::about() -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct ArgValue<'help> { pub(crate) name: &'help str, pub(crate) about: Option<&'help str>, + pub(crate) aliases: Vec<&'help str>, // (name, visible) pub(crate) hidden: bool, } @@ -61,6 +64,37 @@ impl<'help> ArgValue<'help> { Some(self.name) } } + + /// Returns all valid values of the argument value. + /// Namely the name and all aliases. + pub fn get_name_and_aliases(&self) -> impl Iterator { + iter::once(&self.name).chain(&self.aliases).copied() + } + + /// Tests if the value is valid for this argument value + /// + /// The value is valid if it is either the name or one of the aliases. + /// + /// # Examples + /// + /// ```rust + /// # use clap::ArgValue; + /// let arg_value = ArgValue::new("fast").alias("not-slow"); + /// + /// assert!(arg_value.matches("fast", false)); + /// assert!(arg_value.matches("not-slow", false)); + /// + /// assert!(arg_value.matches("FAST", true)); + /// assert!(!arg_value.matches("FAST", false)); + /// ``` + pub fn matches(&self, value: &str, ignore_case: bool) -> bool { + if ignore_case { + self.get_name_and_aliases() + .any(|name| name.eq_ignore_ascii_case(value)) + } else { + self.get_name_and_aliases().any(|name| name == value) + } + } } impl<'help> ArgValue<'help> { @@ -123,4 +157,41 @@ impl<'help> ArgValue<'help> { self.hidden = yes; self } + + /// Sets an alias for this argument value. + /// + /// The alias will be hidden from completion and help texts. + /// + /// # Examples + /// + /// ```rust + /// # use clap::ArgValue; + /// ArgValue::new("slow") + /// .alias("not-fast") + /// # ; + /// ``` + pub fn alias(mut self, name: &'help str) -> Self { + self.aliases.push(name); + self + } + + /// Sets multiple aliases for this argument value. + /// + /// The aliases will be hidden from completion and help texts. + /// + /// # Examples + /// + /// ```rust + /// # use clap::ArgValue; + /// ArgValue::new("slow") + /// .aliases(["not-fast", "snake-like"]) + /// # ; + /// ``` + pub fn aliases(mut self, names: I) -> Self + where + I: IntoIterator, + { + self.aliases.extend(names.into_iter()); + self + } } diff --git a/src/derive.rs b/src/derive.rs index 918f8f06831..133ffbc308e 100644 --- a/src/derive.rs +++ b/src/derive.rs @@ -1,7 +1,7 @@ //! This module contains traits that are usable with the `#[derive(...)].` //! macros in [`clap_derive`]. -use crate::{App, ArgMatches, Error}; +use crate::{App, ArgMatches, ArgValue, Error}; use std::ffi::OsString; @@ -273,7 +273,7 @@ pub trait Subcommand: FromArgMatches + Sized { /// level: Level, /// } /// -/// #[derive(clap::ArgEnum)] +/// #[derive(clap::ArgEnum, Clone)] /// enum Level { /// Debug, /// Info, @@ -281,17 +281,23 @@ pub trait Subcommand: FromArgMatches + Sized { /// Error, /// } /// ``` -pub trait ArgEnum: Sized { - /// All possible argument choices, in display order. - const VARIANTS: &'static [&'static str]; +pub trait ArgEnum: Sized + Clone { + /// All possible argument values, in display order. + fn value_variants<'a>() -> &'a [Self]; /// Parse an argument into `Self`. - fn from_str(input: &str, case_insensitive: bool) -> Result; + fn from_str(input: &str, case_insensitive: bool) -> Result { + Self::value_variants() + .iter() + .find(|v| v.arg_value().unwrap().matches(input, case_insensitive)) + .cloned() + .ok_or_else(|| format!("Invalid variant: {}", input)) + } /// The canonical argument value. /// /// The value is `None` for skipped variants. - fn as_arg(&self) -> Option<&'static str>; + fn arg_value<'a>(&self) -> Option>; } impl Clap for Box { diff --git a/src/parse/validator.rs b/src/parse/validator.rs index 45463a3ecd1..15835ac704c 100644 --- a/src/parse/validator.rs +++ b/src/parse/validator.rs @@ -104,13 +104,10 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> { arg.possible_vals ); let val_str = val.to_string_lossy(); - let ok = if arg.is_set(ArgSettings::IgnoreCase) { - arg.possible_vals - .iter() - .any(|pv| pv.name.eq_ignore_ascii_case(&val_str)) - } else { - arg.possible_vals.iter().any(|pv| pv.name == &*val_str) - }; + let ok = arg + .possible_vals + .iter() + .any(|pv| pv.matches(&val_str, arg.is_set(ArgSettings::IgnoreCase))); if !ok { let used: Vec = matcher .arg_names() diff --git a/tests/possible_values.rs b/tests/possible_values.rs index 932982eb583..5f8638af454 100644 --- a/tests/possible_values.rs +++ b/tests/possible_values.rs @@ -219,6 +219,22 @@ fn possible_values_output() { )); } +#[test] +fn possible_values_alias_output() { + assert!(utils::compare_output( + App::new("test").arg( + Arg::new("option") + .short('O') + .possible_value("slow") + .possible_value(ArgValue::new("fast").alias("fost")) + .possible_value(ArgValue::new("ludicrous speed").aliases(["ls", "lcs"])) + ), + "clap-test -O slo", + PV_ERROR, + true + )); +} + #[test] fn possible_values_hidden_output() { assert!(utils::compare_output( @@ -249,6 +265,42 @@ fn escaped_possible_values_output() { )); } +#[test] +fn alias() { + let m = App::new("pv") + .arg( + Arg::new("option") + .short('o') + .long("--option") + .takes_value(true) + .possible_value(ArgValue::new("test123").alias("123")) + .possible_value("test321") + .case_insensitive(true), + ) + .try_get_matches_from(vec!["pv", "--option", "123"]); + + assert!(m.is_ok()); + assert!(m.unwrap().value_of("option").unwrap().eq("123")); +} + +#[test] +fn aliases() { + let m = App::new("pv") + .arg( + Arg::new("option") + .short('o') + .long("--option") + .takes_value(true) + .possible_value(ArgValue::new("test123").aliases(["1", "2", "3"])) + .possible_value("test321") + .case_insensitive(true), + ) + .try_get_matches_from(vec!["pv", "--option", "2"]); + + assert!(m.is_ok()); + assert!(m.unwrap().value_of("option").unwrap().eq("2")); +} + #[test] fn case_insensitive() { let m = App::new("pv") @@ -272,7 +324,7 @@ fn case_insensitive() { } #[test] -fn case_insensitive_faili() { +fn case_insensitive_fail() { let m = App::new("pv") .arg( Arg::new("option")