Repository: clap-rs/clap
Pull request #3933 (2022-07-14 03:46:15)

feat(clap_derive): Add `default_values_t` and `default_values_os_t` for Vec field types
Implements #3932


emersonford: hm as I coded this up, I realized I could apply this for all `Vec<impl Display>`, not just a `Vec<impl ValueEnum>`. 

this would be out of scope of https://github.com/clap-rs/clap/issues/3932, but I'll push another commit with this change as I feel might as well handle it in the same PR? can always reset back to the single commit and submit another PR otherwise :)
emersonford: @epage just pushed the changes to address all of your comments (aside from the swapping `multi_value` ones as those are no longer relevant when using `default_values_t`)

Patch:
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<T>(iterable: impl IntoIterator<Item = T>) -> 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<Vec<&str>> = 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<T>(iterable: impl IntoIterator<Item = T>) -> Vec<String>
+                                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<Vec<::std::string::String>> = clap::__macro_refs::once_cell::sync::Lazy::new(|| {
+                                    iter_to_vals(#expr)
+                                });
+
+                                static DEFAULT_VALUES: clap::__macro_refs::once_cell::sync::Lazy<Vec<&str>> = 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<T>(iterable: impl IntoIterator<Item = T>) -> 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<Vec<&::std::ffi::OsStr>> = 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<T>(iterable: impl IntoIterator<Item = T>) -> 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<Vec<::std::ffi::OsString>> = clap::__macro_refs::once_cell::sync::Lazy::new(|| {
+                                    iter_to_vals(#expr)
+                                });
+
+                                static DEFAULT_VALUES: clap::__macro_refs::once_cell::sync::Lazy<Vec<&::std::ffi::OsStr>> = 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<Expr>),
+    DefaultValuesT(Ident, Expr),
     DefaultValueOsT(Ident, Option<Expr>),
+    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 [= <expr>]`: [`Arg::default_value`][crate::Arg::default_value] and [`Arg::required(false)`][crate::Arg::required]
 //!   - Requires `std::fmt::Display` or `#[clap(value_enum)]`
 //!   - Without `<expr>`, relies on `Default::default()`
+//! - `default_values_t = <expr>`: [`Arg::default_values`][crate::Arg::default_values] and [`Arg::required(false)`][crate::Arg::required]
+//!   - Requires field arg to be of type `Vec<T>` and `T` to implement `std::fmt::Display` or `#[clap(value_enum)]`
+//!   - `<expr>` must implement `IntoIterator<T>`
 //! - `default_value_os_t [= <expr>]`: [`Arg::default_value_os`][crate::Arg::default_value_os] and [`Arg::required(false)`][crate::Arg::required]
 //!   - Requires `std::convert::Into<OsString>` or `#[clap(value_enum)]`
 //!   - Without `<expr>`, relies on `Default::default()`
+//! - `default_values_os_t = <expr>`: [`Arg::default_values_os`][crate::Arg::default_values_os] and [`Arg::required(false)`][crate::Arg::required]
+//!   - Requires field arg to be of type `Vec<T>` and `T` to implement `std::convert::Into<OsString>` or `#[clap(value_enum)]`
+//!   - `<expr>` must implement `IntoIterator<T>`
 //!
 //! ### Value Enum Attributes
 //!
diff --git a/src/builder/arg.rs b/src/builder/arg.rs
index c4b87265..24ec02d2 100644
--- a/src/builder/arg.rs
+++ b/src/builder/arg.rs
@@ -941,7 +941,7 @@ impl<'help> Arg<'help> {
     /// - It reaches the [maximum number of values]
     /// - It reaches a [specific number of values]
     /// - It finds another flag or option (i.e. something that starts with a `-`)
-    /// - It reaches a [value terminator][Arg::value_terminator]
+    /// - It reaches a [value terminator][Arg::value_terminator] is reached
     ///
     /// Alternatively, [require a delimiter between values][Arg::require_value_delimiter].
     ///
@@ -1768,7 +1768,7 @@ impl<'help> Arg<'help> {
         }
     }
 
-    /// Sentinel to **stop** parsing multiple values of a given argument.
+    /// Sentinel to **stop** parsing multiple values of a give argument.
     ///
     /// By default when
     /// one sets [`multiple_values(true)`] on an argument, clap will continue parsing values for that


Test patch:
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<i32>,
+
+        #[clap(long, default_values_t = &[4, 5, 6])]
+        arg2: Vec<i32>,
+
+        #[clap(long, default_values_t = [7, 8, 9])]
+        arg3: Vec<i32>,
+
+        #[clap(long, default_values_t = 10..=12)]
+        arg4: Vec<i32>,
+
+        #[clap(long, default_values_t = vec!["hello".to_string(), "world".to_string()])]
+        arg5: Vec<String>,
+
+        #[clap(long, default_values_t = &vec!["foo".to_string(), "bar".to_string()])]
+        arg6: Vec<String>,
+    }
+    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::<Opt>();
+    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::<Opt>();
+    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<PathBuf>,
+
+        #[clap(
+            long,
+            default_values_os_t = &[PathBuf::from("bar.baz")]
+        )]
+        arg2: Vec<PathBuf>,
+    }
+    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::<Opt>();
+    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<ArgChoice>,
+
+        #[clap(
+            long,
+            value_enum,
+            default_values_t = clap::ValueEnum::value_variants()
+        )]
+        arg2: Vec<ArgChoice>,
+    }
+
+    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<ArgChoice>,
+
+        #[clap(
+            long,
+            value_enum,
+            default_values_os_t = clap::ValueEnum::value_variants()
+        )]
+        arg2: Vec<ArgChoice>,
+    }
+
+    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])]
+  |            ^^^^^^^^^^^^^^^^