diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 36d2ca935901..f9fb13be845d 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -216,6 +216,45 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_get_many(id)) } + /// Iterate over the values passed to each occurence of an option. + /// + /// Each item is itself an iterator 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_occurrences`]. + /// + /// # 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_occurrences("exec").unwrap().collect(); + /// assert_eq!(vals, [["echo", "hi"], ["echo", "bye"]]); + /// ``` + #[cfg_attr(debug_assertions, track_caller)] + #[cfg(feature = "unstable-grouped")] + pub fn get_occurrences( + &self, + id: &str, + ) -> Option> { + MatchesError::unwrap(id, self.try_get_occurrences(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 +301,61 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_get_raw(id)) } + /// Iterate over the original values for each occurence of an option. + /// + /// Similar to [`ArgMatches::get_occurrences`] 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_occurrences`]. + /// + /// # 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_occurrences("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_occurrences(&self, id: &str) -> Option> { + MatchesError::unwrap(id, self.try_get_raw_occurrences(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 +432,46 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_remove_many(id)) } + /// Return values for each occurrence of an option. + /// + /// Each item is itself an iterator 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_occurrences`]. + /// + /// # 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_occurrences("exec").unwrap().collect(); + /// assert_eq!(vals, [["echo", "hi"], ["echo", "bye"]]); + /// ``` + #[cfg(feature = "unstable-grouped")] + #[cfg_attr(debug_assertions, track_caller)] + pub fn remove_occurrences( + &mut self, + id: &str, + ) -> Option> { + MatchesError::unwrap(id, self.try_remove_occurrences(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 +587,8 @@ impl ArgMatches { /// [`Iterator`]: std::iter::Iterator #[cfg(feature = "unstable-grouped")] #[cfg_attr(debug_assertions, track_caller)] + #[deprecated = "Use get_occurrences or remove_occurrences instead"] + #[allow(deprecated)] pub fn grouped_values_of(&self, id: &str) -> Option { let arg = some!(self.get_arg(id)); let v = GroupedValues { @@ -967,12 +1103,30 @@ impl ArgMatches { let values = arg.vals_flatten(); let values = ValuesRef { // enforced by `try_get_arg_t` - iter: values.map(|v| v.downcast_ref::().expect(INTERNAL_ERROR_MSG)), + iter: values.map(unwrap_downcast_ref), len, }; Ok(Some(values)) } + /// Non-panicking version of [`ArgMatches::get_occurrences`] + #[cfg(feature = "unstable-grouped")] + pub fn try_get_occurrences( + &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(OccurrencesRef { + iter: values.map(|g| OccurrenceValuesRef { + iter: g.iter().map(unwrap_downcast_ref), + }), + })) + } + /// 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 +1142,25 @@ impl ArgMatches { Ok(Some(values)) } + /// Non-panicking version of [`ArgMatches::get_raw_occurrences`] + #[cfg(feature = "unstable-grouped")] + pub fn try_get_raw_occurrences( + &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 occurrences = RawOccurrences { + iter: values.map(|g| RawOccurrenceValues { + iter: g.iter().map(OsString::as_os_str), + }), + }; + Ok(Some(occurrences)) + } + /// Non-panicking version of [`ArgMatches::remove_one`] pub fn try_remove_one( &mut self, @@ -997,7 +1170,7 @@ impl ArgMatches { Some(values) => Ok(values .into_vals_flatten() // enforced by `try_get_arg_t` - .map(|v| v.downcast_into::().expect(INTERNAL_ERROR_MSG)) + .map(unwrap_downcast_into) .next()), None => Ok(None), } @@ -1016,12 +1189,31 @@ impl ArgMatches { let values = arg.into_vals_flatten(); let values = Values { // enforced by `try_get_arg_t` - iter: values.map(|v| v.downcast_into::().expect(INTERNAL_ERROR_MSG)), + iter: values.map(unwrap_downcast_into), len, }; Ok(Some(values)) } + /// Non-panicking version of [`ArgMatches::remove_occurrences`] + #[cfg(feature = "unstable-grouped")] + pub fn try_remove_occurrences( + &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 occurrences = Occurrences { + iter: values.into_iter().map(|g| OccurrenceValues { + iter: g.into_iter().map(unwrap_downcast_into), + }), + }; + Ok(Some(occurrences)) + } + /// Non-panicking version of [`ArgMatches::contains_id`] pub fn try_contains_id(&self, id: &str) -> Result { ok!(self.verify_arg(id)); @@ -1379,12 +1571,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 +1590,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 +1612,91 @@ impl<'a> Default for GroupedValues<'a> { } } +/// Macrot to reduce boilerplate for defining a new Iterator type that simply +/// delegates to a different iterator +macro_rules! delegating_iterator { + ($name:ident<$($t:tt),+> $(where Self: $lf:lifetime)?, $iter_type:ty, $item_type:ty) => { + #[derive(Clone)] + #[allow(missing_debug_implementations)] + pub struct $name<$($t),+> { + #[allow(clippy::type_complexity)] + iter: $iter_type, + } + + impl<$($t),+> Iterator for $name<$($t),+> $(where Self: $lf)? { + type Item = $item_type; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } + } + + impl<$($t),+> DoubleEndedIterator for $name<$($t),+> $(where Self: $lf)? { + fn next_back(&mut self) -> Option { + self.iter.next_back() + } + } + + impl<$($t),+> ExactSizeIterator for $name<$($t),+> $(where Self: $lf)? {} + } +} + +delegating_iterator!(OccurrencesRef<'a, T> where Self: 'a, Map>, fn(&Vec) -> OccurrenceValuesRef<'_, T>>, OccurrenceValuesRef<'a, T>); +delegating_iterator!(OccurrenceValuesRef<'a, T> where Self: 'a, Map, fn(&AnyValue) -> &T>, &'a T); + +impl<'a, T> Default for OccurrencesRef<'a, T> { + fn default() -> Self { + static EMPTY: [Vec; 0] = []; + OccurrencesRef { + iter: EMPTY[..].iter().map(|_| unreachable!()), + } + } +} + +delegating_iterator!( + Occurrences, + Map>, fn(Vec) -> OccurrenceValues>, + OccurrenceValues +); +delegating_iterator!( + OccurrenceValues, + Map, fn(AnyValue) -> T>, + T +); + +impl Default for Occurrences { + fn default() -> Self { + let empty: Vec> = Default::default(); + Occurrences { + iter: empty.into_iter().map(|_| unreachable!()), + } + } +} + +delegating_iterator!( + RawOccurrences<'a>, + Map>, fn(&Vec) -> RawOccurrenceValues<'_>>, + RawOccurrenceValues<'a> +); +delegating_iterator!( + RawOccurrenceValues<'a>, + Map, fn(&OsString) -> &OsStr>, + &'a OsStr +); + +impl<'a> Default for RawOccurrences<'a> { + fn default() -> Self { + static EMPTY: [Vec; 0] = []; + RawOccurrences { + iter: EMPTY[..].iter().map(|_| unreachable!()), + } + } +} + /// Iterate over indices for where an argument appeared when parsing, via [`ArgMatches::indices_of`] /// /// # Examples @@ -1484,6 +1766,14 @@ fn unwrap_string(value: &AnyValue) -> &str { } } +fn unwrap_downcast_ref(value: &AnyValue) -> &T { + value.downcast_ref().expect(INTERNAL_ERROR_MSG) +} + +fn unwrap_downcast_into(value: AnyValue) -> T { + value.downcast_into().expect(INTERNAL_ERROR_MSG) +} + #[cfg(test)] mod tests { use super::*; 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"]]); }