From 04e0ed747451ad9f702fe882ee688074e20c0283 Mon Sep 17 00:00:00 2001 From: Emerson Ford Date: Sun, 24 Jul 2022 16:10:31 -0700 Subject: [PATCH] feat(clap_derive): Add `default_values_t` and `default_values_os_t` --- clap_derive/src/attrs.rs | 153 ++++++++++++++++++ clap_derive/src/parse.rs | 4 + src/_derive/mod.rs | 6 + tests/derive/default_value.rs | 120 ++++++++++++++ tests/derive/value_enum.rs | 102 ++++++++++++ tests/derive_ui/default_values_t_invalid.rs | 13 ++ .../derive_ui/default_values_t_invalid.stderr | 8 + 7 files changed, 406 insertions(+) create mode 100644 tests/derive_ui/default_values_t_invalid.rs create mode 100644 tests/derive_ui/default_values_t_invalid.stderr diff --git a/clap_derive/src/attrs.rs b/clap_derive/src/attrs.rs index 7e8b246f..ecd5048c 100644 --- a/clap_derive/src/attrs.rs +++ b/clap_derive/src/attrs.rs @@ -516,6 +516,81 @@ impl Attrs { self.methods.push(Method::new(raw_ident, val)); } + DefaultValuesT(ident, expr) => { + let ty = if let Some(ty) = self.ty.as_ref() { + ty + } else { + abort!( + ident, + "#[clap(default_values_t)] (without an argument) can be used \ + only on field level"; + + note = "see \ + https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") + }; + + let container_type = Ty::from_syn_ty(ty); + if *container_type != Ty::Vec { + abort!( + ident, + "#[clap(default_values_t)] can be used only on Vec types"; + + note = "see \ + https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") + } + let inner_type = inner_type(ty); + + // Use `Borrow<#inner_type>` so we accept `&Vec<#inner_type>` and + // `Vec<#inner_type>`. + let val = if parsed.iter().any(|a| matches!(a, ValueEnum(_))) { + quote_spanned!(ident.span()=> { + { + fn iter_to_vals(iterable: impl IntoIterator) -> Vec<&'static str> + where + T: ::std::borrow::Borrow<#inner_type> + { + iterable + .into_iter() + .map(|val| { + clap::ValueEnum::to_possible_value(val.borrow()).unwrap().get_name() + }) + .collect() + + } + + static DEFAULT_VALUES: clap::__macro_refs::once_cell::sync::Lazy> = clap::__macro_refs::once_cell::sync::Lazy::new(|| { + iter_to_vals(#expr) + }); + &*DEFAULT_VALUES.as_slice() + } + }) + } else { + quote_spanned!(ident.span()=> { + { + fn iter_to_vals(iterable: impl IntoIterator) -> Vec + where + T: ::std::borrow::Borrow<#inner_type> + { + iterable.into_iter().map(|val| val.borrow().to_string()).collect() + + } + + static DEFAULT_STRINGS: clap::__macro_refs::once_cell::sync::Lazy> = clap::__macro_refs::once_cell::sync::Lazy::new(|| { + iter_to_vals(#expr) + }); + + static DEFAULT_VALUES: clap::__macro_refs::once_cell::sync::Lazy> = clap::__macro_refs::once_cell::sync::Lazy::new(|| { + DEFAULT_STRINGS.iter().map(::std::string::String::as_str).collect() + }); + &*DEFAULT_VALUES.as_slice() + } + }) + }; + + self.methods + .push(Method::new(Ident::new("default_values", ident.span()), val)); + } + DefaultValueOsT(ident, expr) => { let ty = if let Some(ty) = self.ty.as_ref() { ty @@ -556,6 +631,84 @@ impl Attrs { self.methods.push(Method::new(raw_ident, val)); } + DefaultValuesOsT(ident, expr) => { + let ty = if let Some(ty) = self.ty.as_ref() { + ty + } else { + abort!( + ident, + "#[clap(default_values_os_t)] (without an argument) can be used \ + only on field level"; + + note = "see \ + https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") + }; + + let container_type = Ty::from_syn_ty(ty); + if *container_type != Ty::Vec { + abort!( + ident, + "#[clap(default_values_os_t)] can be used only on Vec types"; + + note = "see \ + https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes") + } + let inner_type = inner_type(ty); + + // Use `Borrow<#inner_type>` so we accept `&Vec<#inner_type>` and + // `Vec<#inner_type>`. + let val = if parsed.iter().any(|a| matches!(a, ValueEnum(_))) { + quote_spanned!(ident.span()=> { + { + fn iter_to_vals(iterable: impl IntoIterator) -> Vec<&'static ::std::ffi::OsStr> + where + T: ::std::borrow::Borrow<#inner_type> + { + iterable + .into_iter() + .map(|val| { + clap::ValueEnum::to_possible_value(val.borrow()).unwrap().get_name() + }) + .map(::std::ffi::OsStr::new) + .collect() + + } + + static DEFAULT_VALUES: clap::__macro_refs::once_cell::sync::Lazy> = clap::__macro_refs::once_cell::sync::Lazy::new(|| { + iter_to_vals(#expr) + }); + &*DEFAULT_VALUES.as_slice() + } + }) + } else { + quote_spanned!(ident.span()=> { + { + fn iter_to_vals(iterable: impl IntoIterator) -> Vec<::std::ffi::OsString> + where + T: ::std::borrow::Borrow<#inner_type> + { + iterable.into_iter().map(|val| val.borrow().into()).collect() + + } + + static DEFAULT_OS_STRINGS: clap::__macro_refs::once_cell::sync::Lazy> = clap::__macro_refs::once_cell::sync::Lazy::new(|| { + iter_to_vals(#expr) + }); + + static DEFAULT_VALUES: clap::__macro_refs::once_cell::sync::Lazy> = clap::__macro_refs::once_cell::sync::Lazy::new(|| { + DEFAULT_OS_STRINGS.iter().map(::std::ffi::OsString::as_os_str).collect() + }); + &*DEFAULT_VALUES.as_slice() + } + }) + }; + + self.methods.push(Method::new( + Ident::new("default_values_os", ident.span()), + val, + )); + } + NextDisplayOrder(ident, expr) => { self.next_display_order = Some(Method::new(ident, quote!(#expr))); } diff --git a/clap_derive/src/parse.rs b/clap_derive/src/parse.rs index a194a1ba..d7d4868d 100644 --- a/clap_derive/src/parse.rs +++ b/clap_derive/src/parse.rs @@ -51,7 +51,9 @@ pub enum ClapAttr { // ident = arbitrary_expr NameExpr(Ident, Expr), DefaultValueT(Ident, Option), + DefaultValuesT(Ident, Expr), DefaultValueOsT(Ident, Option), + DefaultValuesOsT(Ident, Expr), NextDisplayOrder(Ident, Expr), NextHelpHeading(Ident, Expr), HelpHeading(Ident, Expr), @@ -120,7 +122,9 @@ impl Parse for ClapAttr { Ok(expr) => match &*name_str { "skip" => Ok(Skip(name, Some(expr))), "default_value_t" => Ok(DefaultValueT(name, Some(expr))), + "default_values_t" => Ok(DefaultValuesT(name, expr)), "default_value_os_t" => Ok(DefaultValueOsT(name, Some(expr))), + "default_values_os_t" => Ok(DefaultValuesOsT(name, expr)), "next_display_order" => Ok(NextDisplayOrder(name, expr)), "next_help_heading" => Ok(NextHelpHeading(name, expr)), "help_heading" => Ok(HelpHeading(name, expr)), diff --git a/src/_derive/mod.rs b/src/_derive/mod.rs index 20d978ab..08ebd7eb 100644 --- a/src/_derive/mod.rs +++ b/src/_derive/mod.rs @@ -223,9 +223,15 @@ //! - `default_value_t [= ]`: [`Arg::default_value`][crate::Arg::default_value] and [`Arg::required(false)`][crate::Arg::required] //! - Requires `std::fmt::Display` or `#[clap(value_enum)]` //! - Without ``, relies on `Default::default()` +//! - `default_values_t = `: [`Arg::default_values`][crate::Arg::default_values] and [`Arg::required(false)`][crate::Arg::required] +//! - Requires field arg to be of type `Vec` and `T` to implement `std::fmt::Display` or `#[clap(value_enum)]` +//! - `` must implement `IntoIterator` //! - `default_value_os_t [= ]`: [`Arg::default_value_os`][crate::Arg::default_value_os] and [`Arg::required(false)`][crate::Arg::required] //! - Requires `std::convert::Into` or `#[clap(value_enum)]` //! - Without ``, relies on `Default::default()` +//! - `default_values_os_t = `: [`Arg::default_values_os`][crate::Arg::default_values_os] and [`Arg::required(false)`][crate::Arg::required] +//! - Requires field arg to be of type `Vec` and `T` to implement `std::convert::Into` or `#[clap(value_enum)]` +//! - `` must implement `IntoIterator` //! //! ### Value Enum Attributes //! diff --git a/tests/derive/default_value.rs b/tests/derive/default_value.rs index db2b7838..70141836 100644 --- a/tests/derive/default_value.rs +++ b/tests/derive/default_value.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::{CommandFactory, Parser}; use crate::utils; @@ -44,6 +46,124 @@ fn auto_default_value_t() { assert!(help.contains("[default: 0]")); } +#[test] +fn default_values_t() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(default_values_t = vec![1, 2, 3])] + arg1: Vec, + + #[clap(long, default_values_t = &[4, 5, 6])] + arg2: Vec, + + #[clap(long, default_values_t = [7, 8, 9])] + arg3: Vec, + + #[clap(long, default_values_t = 10..=12)] + arg4: Vec, + + #[clap(long, default_values_t = vec!["hello".to_string(), "world".to_string()])] + arg5: Vec, + + #[clap(long, default_values_t = &vec!["foo".to_string(), "bar".to_string()])] + arg6: Vec, + } + assert_eq!( + Opt { + arg1: vec![1, 2, 3], + arg2: vec![4, 5, 6], + arg3: vec![7, 8, 9], + arg4: vec![10, 11, 12], + arg5: vec!["hello".to_string(), "world".to_string()], + arg6: vec!["foo".to_string(), "bar".to_string()], + }, + Opt::try_parse_from(&["test"]).unwrap() + ); + assert_eq!( + Opt { + arg1: vec![1], + arg2: vec![4, 5, 6], + arg3: vec![7, 8, 9], + arg4: vec![10, 11, 12], + arg5: vec!["hello".to_string(), "world".to_string()], + arg6: vec!["foo".to_string(), "bar".to_string()], + }, + Opt::try_parse_from(&["test", "1"]).unwrap() + ); + assert_eq!( + Opt { + arg1: vec![1, 2, 3], + arg2: vec![4, 5, 6], + arg3: vec![7, 8, 9], + arg4: vec![42, 15], + arg5: vec!["baz".to_string()], + arg6: vec!["foo".to_string(), "bar".to_string()], + }, + Opt::try_parse_from(&["test", "--arg4", "42", "--arg4", "15", "--arg5", "baz"]).unwrap() + ); + + let help = utils::get_long_help::(); + assert!(help.contains("[default: 1 2 3]")); +} + +#[test] +fn default_value_os_t() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(default_value_os_t = PathBuf::from("abc.def"))] + arg: PathBuf, + } + assert_eq!( + Opt { + arg: PathBuf::from("abc.def") + }, + Opt::try_parse_from(&["test"]).unwrap() + ); + assert_eq!( + Opt { + arg: PathBuf::from("ghi") + }, + Opt::try_parse_from(&["test", "ghi"]).unwrap() + ); + + let help = utils::get_long_help::(); + assert!(help.contains("[default: abc.def]")); +} + +#[test] +fn default_values_os_t() { + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap( + default_values_os_t = vec![PathBuf::from("abc.def"), PathBuf::from("123.foo")] + )] + arg1: Vec, + + #[clap( + long, + default_values_os_t = &[PathBuf::from("bar.baz")] + )] + arg2: Vec, + } + assert_eq!( + Opt { + arg1: vec![PathBuf::from("abc.def"), PathBuf::from("123.foo")], + arg2: vec![PathBuf::from("bar.baz")] + }, + Opt::try_parse_from(&["test"]).unwrap() + ); + assert_eq!( + Opt { + arg1: vec![PathBuf::from("ghi")], + arg2: vec![PathBuf::from("baz.bar"), PathBuf::from("foo.bar")] + }, + Opt::try_parse_from(&["test", "ghi", "--arg2", "baz.bar", "--arg2", "foo.bar"]).unwrap() + ); + + let help = utils::get_long_help::(); + assert!(help.contains("[default: abc.def 123.foo]")); +} + #[test] fn detect_os_variant() { #![allow(unused_parens)] // needed for `as_ref` call diff --git a/tests/derive/value_enum.rs b/tests/derive/value_enum.rs index ca9faaa5..1eae3116 100644 --- a/tests/derive/value_enum.rs +++ b/tests/derive/value_enum.rs @@ -78,6 +78,108 @@ fn default_value() { ); } +#[test] +fn vec_for_default_values_t() { + #[derive(clap::ValueEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(value_enum, default_values_t = vec![ArgChoice::Foo, ArgChoice::Bar])] + arg1: Vec, + + #[clap( + long, + value_enum, + default_values_t = clap::ValueEnum::value_variants() + )] + arg2: Vec, + } + + assert_eq!( + Opt { + arg1: vec![ArgChoice::Foo], + arg2: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg1: vec![ArgChoice::Bar], + arg2: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&["", "bar"]).unwrap() + ); + assert_eq!( + Opt { + arg1: vec![ArgChoice::Foo, ArgChoice::Bar], + arg2: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&[""]).unwrap() + ); + assert_eq!( + Opt { + arg1: vec![ArgChoice::Foo, ArgChoice::Bar], + arg2: vec![ArgChoice::Foo] + }, + Opt::try_parse_from(&["", "--arg2", "foo"]).unwrap() + ); +} + +#[test] +fn vec_for_default_values_os_t() { + #[derive(clap::ValueEnum, PartialEq, Debug, Clone)] + enum ArgChoice { + Foo, + Bar, + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[clap(value_enum, default_values_os_t = vec![ArgChoice::Foo, ArgChoice::Bar])] + arg: Vec, + + #[clap( + long, + value_enum, + default_values_os_t = clap::ValueEnum::value_variants() + )] + arg2: Vec, + } + + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo], + arg2: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&["", "foo"]).unwrap() + ); + assert_eq!( + Opt { + arg: vec![ArgChoice::Bar], + arg2: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&["", "bar"]).unwrap() + ); + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo, ArgChoice::Bar], + arg2: vec![ArgChoice::Foo, ArgChoice::Bar] + }, + Opt::try_parse_from(&[""]).unwrap() + ); + assert_eq!( + Opt { + arg: vec![ArgChoice::Foo, ArgChoice::Bar], + arg2: vec![ArgChoice::Foo] + }, + Opt::try_parse_from(&["", "--arg2", "foo"]).unwrap() + ); +} + #[test] fn multi_word_is_renamed_kebab() { #[derive(clap::ValueEnum, PartialEq, Debug, Clone)] diff --git a/tests/derive_ui/default_values_t_invalid.rs b/tests/derive_ui/default_values_t_invalid.rs new file mode 100644 index 00000000..0c99b39e --- /dev/null +++ b/tests/derive_ui/default_values_t_invalid.rs @@ -0,0 +1,13 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[clap(name = "basic")] +struct Opt { + #[clap(default_values_t = [1, 2, 3])] + value: u32, +} + +fn main() { + let opt = Opt::parse(); + println!("{:?}", opt); +} diff --git a/tests/derive_ui/default_values_t_invalid.stderr b/tests/derive_ui/default_values_t_invalid.stderr new file mode 100644 index 00000000..ede032f8 --- /dev/null +++ b/tests/derive_ui/default_values_t_invalid.stderr @@ -0,0 +1,8 @@ +error: #[clap(default_values_t)] can be used only on Vec types + + = note: see https://github.com/clap-rs/clap/blob/master/examples/derive_ref/README.md#magic-attributes + + --> tests/derive_ui/default_values_t_invalid.rs:6:12 + | +6 | #[clap(default_values_t = [1, 2, 3])] + | ^^^^^^^^^^^^^^^^