Repository: clap-rs/clap
Pull request #4600 (2023-01-03 06:29:36)

Add support for deriving grouped options for Vec<Vec<T>>
Relates-To: #2924

<!--
Thanks for helping out!

Please link the appropriate issue from your PR.

If you don't have an issue, we'd recommend starting with one first so the PR can focus on the
implementation (unless its an obvious bug or documentation fix that will have
little conversation).
-->



Patch:
diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs
index ad2a6463..1c3ce8b2 100644
--- a/clap_derive/src/derives/args.rs
+++ b/clap_derive/src/derives/args.rs
@@ -288,6 +288,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() && !override_required;
                         // `ArgAction::takes_values` is assuming `ArgAction::default_value` will be
@@ -431,7 +439,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",
@@ -470,7 +480,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",
@@ -609,6 +621,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));
 
@@ -665,6 +678,17 @@ fn gen_parsers(
             }
         }
 
+        Ty::VecVec => quote_spanned! { ty.span()=>
+            #arg_matches.#get_occurrences(#id)
+                .map(|g| g.map(::std::iter::Iterator::collect).collect::<Vec<Vec<_>>>())
+                .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::<Vec<Vec<_>>>())
+        },
+
         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 a068e360..5e8272ac 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 1cf05144..a87ab169 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<T>",
             Self::OptionOption => "Option<Option<T>>",
             Self::OptionVec => "Option<Vec<T>>",
+            Self::VecVec => "Vec<Vec<T>>",
+            Self::OptionVecVec => "Option<Vec<Vec<T>>>",
             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<Ty> {
+    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<Ty> {
+    is_generic_ty(ty, "Vec").then(|| vec_ty)
+}


Test patch:
diff --git a/tests/derive/main.rs b/tests/derive/main.rs
index 0c6ea7bd..072aab96 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 00000000..db4ae37e
--- /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<Vec<i32>>,
+    }
+
+    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<Vec<String>, 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<String>>,
+    }
+
+    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<Vec<i32>>,
+    }
+
+    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<Vec<Vec<i32>>>,
+    }
+
+    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<Vec<Vec<i32>>>,
+    }
+
+    assert_eq!(
+        Opt { points: None },
+        Opt::try_parse_from(&["test"]).unwrap()
+    );
+}