diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 36d2ca935901..8f911bcc67f5 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -216,6 +216,47 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_get_many(id)) } + /// Iterate over groups of values of a specific option. + /// + /// specifically grouped by the occurrences of the options. + /// + /// Each group is a `Vec<&T>` containing the arguments passed to a single occurrence + /// of the option. + /// + /// If the option doesn't support multiple occurrences, or there was only a single occurrence, + /// the iterator will only contain a single item. + /// + /// Returns `None` if the option wasn't present. + /// + /// # Panics + /// + /// If the argument definition and access mismatch. To handle this case programmatically, see + /// [`ArgMatches::try_get_groups`]. + /// + /// # Examples + /// ```rust + /// # use clap::{Command,Arg, ArgAction, value_parser}; + /// let m = Command::new("myprog") + /// .arg(Arg::new("exec") + /// .short('x') + /// .num_args(1..) + /// .action(ArgAction::Append) + /// .value_parser(value_parser!(String)) + /// .value_terminator(";")) + /// .get_matches_from(vec![ + /// "myprog", "-x", "echo", "hi", ";", "-x", "echo", "bye"]); + /// let vals: Vec> = m.get_groups("exec").unwrap().collect(); + /// assert_eq!(vals, [["echo", "hi"], ["echo", "bye"]]); + /// ``` + #[cfg_attr(debug_assertions, track_caller)] + #[cfg(feature = "unstable-grouped")] + pub fn get_groups( + &self, + id: &str, + ) -> Option> { + MatchesError::unwrap(id, self.try_get_groups(id)) + } + /// Iterate over the original argument values. /// /// An `OsStr` on Unix-like systems is any series of bytes, regardless of whether or not they @@ -262,6 +303,61 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_get_raw(id)) } + /// Iterate over the original argument values in groups. + /// + /// Similar to [`ArgMatches::get_groups`] but returns raw values. + /// + /// An `OsStr` on Unix-like systems is any series of bytes, regardless of whether or not they + /// contain valid UTF-8. Since [`String`]s in Rust are guaranteed to be valid UTF-8, a valid + /// filename on a Unix system as an argument value may contain invalid UTF-8. + /// + /// Returns `None` if the option wasn't present. + /// + /// # Panic + /// + /// If the argument definition and access mismatch. To handle this case programmatically, see + /// [`ArgMatches::try_get_raw_groups`]. + /// + /// # Examples + /// + #[cfg_attr(not(unix), doc = " ```ignore")] + #[cfg_attr(unix, doc = " ```")] + /// # use clap::{Command, arg, value_parser, ArgAction, Arg}; + /// # use std::ffi::{OsStr,OsString}; + /// # use std::os::unix::ffi::{OsStrExt,OsStringExt}; + /// use std::path::PathBuf; + /// + /// let m = Command::new("myprog") + /// .arg(Arg::new("exec") + /// .short('x') + /// .num_args(1..) + /// .action(ArgAction::Append) + /// .value_parser(value_parser!(PathBuf)) + /// .value_terminator(";")) + /// .get_matches_from(vec![OsString::from("myprog"), + /// OsString::from("-x"), + /// OsString::from("echo"), OsString::from("hi"), OsString::from(";"), + /// OsString::from("-x"), + /// OsString::from("echo"), + /// // "{0xe9}!" + /// OsString::from_vec(vec![0xe9, b'!'])]); + /// let mut itr = m.get_raw_groups("exec") + /// .expect("`-x`is required") + /// .into_iter(); + /// assert_eq!(itr.next(), Some(vec![OsStr::new("echo"), OsStr::new("hi")])); + /// assert_eq!(itr.next(), Some(vec![OsStr::new("echo"), OsStr::from_bytes(&[0xe9, b'!'])])); + /// assert_eq!(itr.next(), None); + /// ``` + /// [`Iterator`]: std::iter::Iterator + /// [`OsStr`]: std::ffi::OsStr + /// [values]: OsValues + /// [`String`]: std::string::String + #[cfg(feature = "unstable-grouped")] + #[cfg_attr(debug_assertions, track_caller)] + pub fn get_raw_groups(&self, id: &str) -> Option> { + MatchesError::unwrap(id, self.try_get_raw_groups(id)) + } + /// Returns the value of a specific option or positional argument. /// /// i.e. an argument that [takes an additional value][crate::Arg::num_args] at runtime. @@ -338,6 +434,48 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_remove_many(id)) } + /// Return groups of values of a specific option. + /// + /// specifically grouped by the occurrences of the options. + /// + /// Each group is a `Vec` containing the arguments passed to a single occurrence of + /// the option. + /// + /// If the option doesn't support multiple occurrences, or there was only a single occurrence, + /// the iterator will only contain a single item. + /// + /// Returns `None` if the option wasn't present. + /// + /// # Panic + /// + /// If the argument definition and access mismatch. To handle this case programmatically, see + /// [`ArgMatches::try_remove_groups`]. + /// + /// # Examples + /// + /// ```rust + /// # use clap::{Command, Arg, value_parser, ArgAction}; + /// let mut m = Command::new("myprog") + /// .arg(Arg::new("exec") + /// .short('x') + /// .num_args(1..) + /// .action(ArgAction::Append) + /// .value_parser(value_parser!(String)) + /// .value_terminator(";")) + /// .get_matches_from(vec![ + /// "myprog", "-x", "echo", "hi", ";", "-x", "echo", "bye"]); + /// let vals: Vec> = m.remove_groups("exec").unwrap().collect(); + /// assert_eq!(vals, [["echo", "hi"], ["echo", "bye"]]); + /// ``` + #[cfg(feature = "unstable-grouped")] + #[cfg_attr(debug_assertions, track_caller)] + pub fn remove_groups( + &mut self, + id: &str, + ) -> Option> { + MatchesError::unwrap(id, self.try_remove_groups(id)) + } + /// Check if values are present for the argument or group id /// /// *NOTE:* This will always return `true` if [`default_value`] has been set. @@ -453,6 +591,8 @@ impl ArgMatches { /// [`Iterator`]: std::iter::Iterator #[cfg(feature = "unstable-grouped")] #[cfg_attr(debug_assertions, track_caller)] + #[deprecated = "Use get_groups, remove_groups, or get_raw_groups instead"] + #[allow(deprecated)] pub fn grouped_values_of(&self, id: &str) -> Option { let arg = some!(self.get_arg(id)); let v = GroupedValues { @@ -973,6 +1113,26 @@ impl ArgMatches { Ok(Some(values)) } + /// Non-panicking version of [`ArgMatches::get_groups`] + #[cfg(feature = "unstable-grouped")] + pub fn try_get_groups( + &self, + id: &str, + ) -> Result>, MatchesError> { + let arg = match ok!(self.try_get_arg_t::(id)) { + Some(arg) => arg, + None => return Ok(None), + }; + let values = arg.vals(); + Ok(Some(GroupsRef { + iter: values.map(|g| { + g.iter() + .map(|v| v.downcast_ref::().expect(INTERNAL_ERROR_MSG)) + .collect() + }), + })) + } + /// Non-panicking version of [`ArgMatches::get_raw`] pub fn try_get_raw(&self, id: &str) -> Result>, MatchesError> { let arg = match ok!(self.try_get_arg(id)) { @@ -988,6 +1148,20 @@ impl ArgMatches { Ok(Some(values)) } + /// Non-panicking version of [`ArgMatches::get_raw_groups`] + #[cfg(feature = "unstable-grouped")] + pub fn try_get_raw_groups(&self, id: &str) -> Result>, MatchesError> { + let arg = match ok!(self.try_get_arg(id)) { + Some(arg) => arg, + None => return Ok(None), + }; + let values = arg.raw_vals(); + let groups = RawGroups { + iter: values.map(|g| g.iter().map(OsString::as_os_str).collect()), + }; + Ok(Some(groups)) + } + /// Non-panicking version of [`ArgMatches::remove_one`] pub fn try_remove_one( &mut self, @@ -1022,6 +1196,27 @@ impl ArgMatches { Ok(Some(values)) } + /// Non-panicking version of [`ArgMatches::remove_groups`] + #[cfg(feature = "unstable-groups")] + pub fn try_remove_groups( + &mut self, + id: &str, + ) -> Result>, MatchesError> { + let arg = match ok!(self.try_remove_arg_t::(id)) { + Some(arg) => arg, + None => return Ok(None), + }; + let values = arg.into_vals(); + let groups = Groups { + iter: values.into_iter().map(|g| { + g.into_iter() + .map(|v| v.downcast_into::().expect(INTERNAL_ERROR_MSG)) + .collect() + }), + }; + Ok(Some(groups)) + } + /// Non-panicking version of [`ArgMatches::contains_id`] pub fn try_contains_id(&self, id: &str) -> Result { ok!(self.verify_arg(id)); @@ -1379,12 +1574,14 @@ impl Default for RawValues<'_> { #[derive(Clone)] #[allow(missing_debug_implementations)] +#[deprecated] pub struct GroupedValues<'a> { #[allow(clippy::type_complexity)] iter: Map>, fn(&Vec) -> Vec<&str>>, len: usize, } +#[allow(deprecated)] impl<'a> Iterator for GroupedValues<'a> { type Item = Vec<&'a str>; @@ -1396,15 +1593,18 @@ impl<'a> Iterator for GroupedValues<'a> { } } +#[allow(deprecated)] impl<'a> DoubleEndedIterator for GroupedValues<'a> { fn next_back(&mut self) -> Option { self.iter.next_back() } } +#[allow(deprecated)] impl<'a> ExactSizeIterator for GroupedValues<'a> {} /// Creates an empty iterator. Used for `unwrap_or_default()`. +#[allow(deprecated)] impl<'a> Default for GroupedValues<'a> { fn default() -> Self { static EMPTY: [Vec; 0] = []; @@ -1415,6 +1615,111 @@ impl<'a> Default for GroupedValues<'a> { } } +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct GroupsRef<'a, T> { + #[allow(clippy::type_complexity)] + iter: Map>, fn(&Vec) -> Vec<&T>>, +} + +impl<'a, T> Iterator for GroupsRef<'a, T> +where + Self: 'a, +{ + type Item = Vec<&'a T>; // should this be an iterator instead? + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a, T> DoubleEndedIterator for GroupsRef<'a, T> +where + Self: 'a, +{ + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl<'a, T> ExactSizeIterator for GroupsRef<'a, T> where Self: 'a {} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct Groups { + #[allow(clippy::type_complexity)] + iter: Map>, fn(Vec) -> Vec>, +} + +impl Iterator for Groups { + type Item = Vec; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl DoubleEndedIterator for Groups { + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl ExactSizeIterator for Groups {} + +impl Default for Groups { + fn default() -> Self { + let empty: Vec> = Default::default(); + Groups { + iter: empty.into_iter().map(|_| unreachable!()), + } + } +} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct RawGroups<'a> { + #[allow(clippy::type_complexity)] + iter: Map>, fn(&Vec) -> Vec<&OsStr>>, +} + +impl<'a> Iterator for RawGroups<'a> { + type Item = Vec<&'a OsStr>; + + fn next(&mut self) -> Option { + self.iter.next() + } + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a> DoubleEndedIterator for RawGroups<'a> { + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl<'a> ExactSizeIterator for RawGroups<'a> {} + +/// Creates an empty iterator. Used for `unwrap_or_default()`. +impl<'a> Default for RawGroups<'a> { + fn default() -> Self { + static EMPTY: [Vec; 0] = []; + RawGroups { + iter: EMPTY[..].iter().map(|_| unreachable!()), + } + } +} + /// Iterate over indices for where an argument appeared when parsing, via [`ArgMatches::indices_of`] /// /// # Examples diff --git a/src/parser/matches/matched_arg.rs b/src/parser/matches/matched_arg.rs index 6a80625f856a..188f82e6144c 100644 --- a/src/parser/matches/matched_arg.rs +++ b/src/parser/matches/matched_arg.rs @@ -80,6 +80,11 @@ impl MatchedArg { self.vals.iter() } + #[cfg(feature = "unstable-grouped")] + pub(crate) fn into_vals(self) -> Vec> { + self.vals + } + pub(crate) fn vals_flatten(&self) -> Flatten>> { self.vals.iter().flatten() } @@ -88,6 +93,11 @@ impl MatchedArg { self.vals.into_iter().flatten() } + #[cfg(feature = "unstable-grouped")] + pub(crate) fn raw_vals(&self) -> Iter> { + self.raw_vals.iter() + } + pub(crate) fn raw_vals_flatten(&self) -> Flatten>> { self.raw_vals.iter().flatten() } diff --git a/tests/builder/grouped_values.rs b/tests/builder/grouped_values.rs index 67df706ef547..b7a688bad0da 100644 --- a/tests/builder/grouped_values.rs +++ b/tests/builder/grouped_values.rs @@ -22,7 +22,7 @@ fn grouped_value_works() { "en_US:my option 2", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("option").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("option").unwrap().collect(); assert_eq!( grouped_vals, vec![ @@ -50,7 +50,7 @@ fn issue_1026() { "target3", "file8", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("target").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("target").unwrap().collect(); assert_eq!( grouped_vals, vec![ @@ -80,7 +80,7 @@ fn grouped_value_long_flag_delimiter() { "alice,bob", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("option").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("option").unwrap().collect(); assert_eq!( grouped_vals, vec![ @@ -104,7 +104,7 @@ fn grouped_value_short_flag_delimiter() { ) .try_get_matches_from(vec!["myapp", "-o=foo", "-o=val1,val2,val3", "-o=bar"]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("option").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("option").unwrap().collect(); assert_eq!( grouped_vals, vec![vec!["foo"], vec!["val1", "val2", "val3"], vec!["bar"]] @@ -124,7 +124,7 @@ fn grouped_value_positional_arg() { "myprog", "val1", "val2", "val3", "val4", "val5", "val6", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("pos").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("pos").unwrap().collect(); assert_eq!( grouped_vals, vec![vec!["val1", "val2", "val3", "val4", "val5", "val6"]] @@ -145,7 +145,7 @@ fn grouped_value_multiple_positional_arg() { "myprog", "val1", "val2", "val3", "val4", "val5", "val6", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("pos2").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("pos2").unwrap().collect(); assert_eq!( grouped_vals, vec![vec!["val2", "val3", "val4", "val5", "val6"]] @@ -167,7 +167,7 @@ fn grouped_value_multiple_positional_arg_last_multiple() { "myprog", "val1", "--", "val2", "val3", "val4", "val5", "val6", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("pos2").unwrap().collect(); + let grouped_vals: Vec<_> = m.get_groups::("pos2").unwrap().collect(); assert_eq!( grouped_vals, vec![vec!["val2", "val3", "val4", "val5", "val6"]] @@ -190,10 +190,10 @@ fn grouped_interleaved_positional_values() { .try_get_matches_from(["foo", "1", "2", "-f", "a", "3", "-f", "b", "4"]) .unwrap(); - let pos: Vec<_> = m.grouped_values_of("pos").unwrap().collect(); + let pos: Vec<_> = m.get_groups::("pos").unwrap().collect(); assert_eq!(pos, vec![vec!["1", "2"], vec!["3"], vec!["4"]]); - let flag: Vec<_> = m.grouped_values_of("flag").unwrap().collect(); + let flag: Vec<_> = m.get_groups::("flag").unwrap().collect(); assert_eq!(flag, vec![vec!["a"], vec!["b"]]); } @@ -213,10 +213,10 @@ fn grouped_interleaved_positional_occurrences() { .try_get_matches_from(["foo", "1", "2", "-f", "a", "3", "-f", "b", "4"]) .unwrap(); - let pos: Vec<_> = m.grouped_values_of("pos").unwrap().collect(); + let pos: Vec<_> = m.get_groups::("pos").unwrap().collect(); assert_eq!(pos, vec![vec!["1", "2"], vec!["3"], vec!["4"]]); - let flag: Vec<_> = m.grouped_values_of("flag").unwrap().collect(); + let flag: Vec<_> = m.get_groups::("flag").unwrap().collect(); assert_eq!(flag, vec![vec!["a"], vec!["b"]]); }