diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs index ee16eaacf22..19df8111ee6 100644 --- a/clap_derive/src/derives/args.rs +++ b/clap_derive/src/derives/args.rs @@ -298,6 +298,14 @@ pub fn gen_augment( } } + Ty::VecVec | Ty::OptionVecVec => { + quote_spanned! { ty.span() => + .value_name(#value_name) + #value_parser + #action + } + } + Ty::Other => { let required = item.find_default_method().is_none(); // `ArgAction::takes_values` is assuming `ArgAction::default_value` will be @@ -452,7 +460,9 @@ pub fn gen_constructor(fields: &[(&Field, Item)]) -> TokenStream { Ty::Unit | Ty::Vec | Ty::OptionOption | - Ty::OptionVec => { + Ty::OptionVec | + Ty::VecVec | + Ty::OptionVecVec => { abort!( ty.span(), "{} types are not supported for subcommand", @@ -491,7 +501,9 @@ pub fn gen_constructor(fields: &[(&Field, Item)]) -> TokenStream { Ty::Unit | Ty::Vec | Ty::OptionOption | - Ty::OptionVec => { + Ty::OptionVec | + Ty::VecVec | + Ty::OptionVecVec => { abort!( ty.span(), "{} types are not supported for flatten", @@ -630,6 +642,7 @@ fn gen_parsers( let id = item.id(); let get_one = quote_spanned!(span=> remove_one::<#convert_type>); let get_many = quote_spanned!(span=> remove_many::<#convert_type>); + let get_occurrences = quote_spanned!(span=> remove_occurrences::<#convert_type>); let deref = quote!(|s| s); let parse = quote_spanned!(span=> |s| ::std::result::Result::Ok::<_, clap::Error>(s)); @@ -686,6 +699,17 @@ fn gen_parsers( } } + Ty::VecVec => quote_spanned! { ty.span()=> + #arg_matches.#get_occurrences(#id) + .map(|g| g.map(::std::iter::Iterator::collect).collect::>>()) + .unwrap_or_else(Vec::new) + }, + + Ty::OptionVecVec => quote_spanned! { ty.span()=> + #arg_matches.#get_occurrences(#id) + .map(|g| g.map(::std::iter::Iterator::collect).collect::>>()) + }, + Ty::Other => { quote_spanned! { ty.span()=> #arg_matches.#get_one(#id) diff --git a/clap_derive/src/item.rs b/clap_derive/src/item.rs index a068e36011a..5e8272ac0c1 100644 --- a/clap_derive/src/item.rs +++ b/clap_derive/src/item.rs @@ -1121,7 +1121,7 @@ impl Action { fn default_action(field_type: &Type, span: Span) -> Method { let ty = Ty::from_syn_ty(field_type); let args = match *ty { - Ty::Vec | Ty::OptionVec => { + Ty::Vec | Ty::OptionVec | Ty::VecVec | Ty::OptionVecVec => { quote_spanned! { span=> clap::ArgAction::Append } diff --git a/clap_derive/src/utils/ty.rs b/clap_derive/src/utils/ty.rs index 1cf051441f1..a87ab16962f 100644 --- a/clap_derive/src/utils/ty.rs +++ b/clap_derive/src/utils/ty.rs @@ -11,9 +11,11 @@ use syn::{ pub enum Ty { Unit, Vec, + VecVec, Option, OptionOption, OptionVec, + OptionVecVec, Other, } @@ -24,13 +26,13 @@ impl Ty { if is_unit_ty(ty) { t(Unit) - } else if is_generic_ty(ty, "Vec") { - t(Vec) + } else if let Some(vt) = get_vec_ty(ty, Vec, VecVec) { + t(vt) } else if let Some(subty) = subty_if_name(ty, "Option") { if is_generic_ty(subty, "Option") { t(OptionOption) - } else if is_generic_ty(subty, "Vec") { - t(OptionVec) + } else if let Some(vt) = get_vec_ty(subty, OptionVec, OptionVecVec) { + t(vt) } else { t(Option) } @@ -46,6 +48,8 @@ impl Ty { Self::Option => "Option", Self::OptionOption => "Option>", Self::OptionVec => "Option>", + Self::VecVec => "Vec>", + Self::OptionVecVec => "Option>>", Self::Other => "...other...", } } @@ -55,9 +59,13 @@ pub fn inner_type(field_ty: &syn::Type) -> &syn::Type { let ty = Ty::from_syn_ty(field_ty); match *ty { Ty::Vec | Ty::Option => sub_type(field_ty).unwrap_or(field_ty), - Ty::OptionOption | Ty::OptionVec => { + Ty::OptionOption | Ty::OptionVec | Ty::VecVec => { sub_type(field_ty).and_then(sub_type).unwrap_or(field_ty) } + Ty::OptionVecVec => sub_type(field_ty) + .and_then(sub_type) + .and_then(sub_type) + .unwrap_or(field_ty), _ => field_ty, } } @@ -139,3 +147,19 @@ where { iter.next().filter(|_| iter.next().is_none()) } + +#[cfg(feature = "unstable-v5")] +fn get_vec_ty(ty: &Type, vec_ty: Ty, vecvec_ty: Ty) -> Option { + subty_if_name(ty, "Vec").map(|subty| { + if is_generic_ty(subty, "Vec") { + vecvec_ty + } else { + vec_ty + } + }) +} + +#[cfg(not(feature = "unstable-v5"))] +fn get_vec_ty(ty: &Type, vec_ty: Ty, _vecvec_ty: Ty) -> Option { + is_generic_ty(ty, "Vec").then(|| vec_ty) +} diff --git a/tests/derive/main.rs b/tests/derive/main.rs index 0c6ea7bd75b..072aab96940 100644 --- a/tests/derive/main.rs +++ b/tests/derive/main.rs @@ -22,6 +22,7 @@ mod macros; mod naming; mod nested_subcommands; mod non_literal_attributes; +mod occurrences; mod options; mod privacy; mod raw_bool_literal; diff --git a/tests/derive/occurrences.rs b/tests/derive/occurrences.rs new file mode 100644 index 00000000000..db4ae37eb03 --- /dev/null +++ b/tests/derive/occurrences.rs @@ -0,0 +1,82 @@ +#![cfg(all(feature = "unstable-grouped", feature = "unstable-v5"))] +use clap::Parser; + +#[test] +fn test_vec_of_vec() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[arg(short = 'p', num_args = 2)] + points: Vec>, + } + + assert_eq!( + Opt { + points: vec![vec![1, 2], vec![0, 0]] + }, + Opt::try_parse_from(&["test", "-p", "1", "2", "-p", "0", "0"]).unwrap() + ); +} + +#[test] +fn test_vec_of_vec_opt_out() { + fn parser(s: &str) -> Result, std::convert::Infallible> { + Ok(s.split(',').map(str::to_owned).collect()) + } + + #[derive(Parser, PartialEq, Debug)] + struct Opt { + #[arg(value_parser = parser, short = 'p')] + arg: Vec<::std::vec::Vec>, + } + + assert_eq!( + Opt { + arg: vec![vec!["1".into(), "2".into()], vec!["a".into(), "b".into()]], + }, + Opt::try_parse_from(["test", "-p", "1,2", "-p", "a,b"]).unwrap(), + ); +} + +#[test] +fn test_vec_vec_empty() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[arg(short = 'p', num_args = 2)] + points: Vec>, + } + + assert_eq!( + Opt { points: vec![] }, + Opt::try_parse_from(&["test"]).unwrap() + ); +} + +#[test] +fn test_option_vec_vec() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[arg(short = 'p', num_args = 2)] + points: Option>>, + } + + assert_eq!( + Opt { + points: Some(vec![vec![1, 2], vec![3, 4]]) + }, + Opt::try_parse_from(&["test", "-p", "1", "2", "-p", "3", "4"]).unwrap() + ); +} + +#[test] +fn test_option_vec_vec_empty() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + #[arg(short = 'p', num_args = 2)] + points: Option>>, + } + + assert_eq!( + Opt { points: None }, + Opt::try_parse_from(&["test"]).unwrap() + ); +}