diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 36d2ca935901..80bec7a8d317 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -216,6 +216,44 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_get_many(id)) } + /// Iterate over the values passed to 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. + /// + /// # 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("x") + /// .short('x') + /// .num_args(2) + /// .action(ArgAction::Append) + /// .value_parser(value_parser!(String))) + /// .get_matches_from(vec![ + /// "myprog", "-x", "a", "b", "-x", "c", "d"]); + /// let vals: Vec> = m.get_occurrences("x").unwrap().map(Iterator::collect).collect(); + /// assert_eq!(vals, [["a", "b"], ["c", "d"]]); + /// ``` + #[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 +300,60 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_get_raw(id)) } + /// Iterate over the original values for each occurrence 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("x") + /// .short('x') + /// .num_args(2) + /// .action(ArgAction::Append) + /// .value_parser(value_parser!(PathBuf))) + /// .get_matches_from(vec![OsString::from("myprog"), + /// OsString::from("-x"), + /// OsString::from("a"), OsString::from("b"), + /// OsString::from("-x"), + /// OsString::from("c"), + /// // "{0xe9}!" + /// OsString::from_vec(vec![0xe9, b'!'])]); + /// let mut itr = m.get_raw_occurrences("x") + /// .expect("`-x`is required") + /// .map(Iterator::collect::>); + /// assert_eq!(itr.next(), Some(vec![OsStr::new("a"), OsStr::new("b")])); + /// assert_eq!(itr.next(), Some(vec![OsStr::new("c"), 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 +430,45 @@ 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("x") + /// .short('x') + /// .num_args(2) + /// .action(ArgAction::Append) + /// .value_parser(value_parser!(String))) + /// .get_matches_from(vec![ + /// "myprog", "-x", "a", "b", "-x", "c", "d"]); + /// let vals: Vec> = m.remove_occurrences("x").unwrap().map(Iterator::collect).collect(); + /// assert_eq!(vals, [["a", "b"], ["c", "d"]]); + /// ``` + #[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 +584,11 @@ impl ArgMatches { /// [`Iterator`]: std::iter::Iterator #[cfg(feature = "unstable-grouped")] #[cfg_attr(debug_assertions, track_caller)] + #[deprecated( + since = "4.1.0", + note = "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(since = "4.1.0", note = "Use Occurrences instead")] 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,212 @@ impl<'a> Default for GroupedValues<'a> { } } +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct Occurrences { + #[allow(clippy::type_complexity)] + iter: Map>, fn(Vec) -> OccurrenceValues>, +} + +impl Iterator for Occurrences { + type Item = OccurrenceValues; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl DoubleEndedIterator for Occurrences { + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl ExactSizeIterator for Occurrences {} + +impl Default for Occurrences { + fn default() -> Self { + let empty: Vec> = Default::default(); + Occurrences { + iter: empty.into_iter().map(|_| unreachable!()), + } + } +} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct OccurrenceValues { + #[allow(clippy::type_complexity)] + iter: Map, fn(AnyValue) -> T>, +} + +impl Iterator for OccurrenceValues { + type Item = T; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl DoubleEndedIterator for OccurrenceValues { + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl ExactSizeIterator for OccurrenceValues {} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct OccurrencesRef<'a, T> { + #[allow(clippy::type_complexity)] + iter: Map>, fn(&Vec) -> OccurrenceValuesRef<'_, T>>, +} + +impl<'a, T> Iterator for OccurrencesRef<'a, T> +where + Self: 'a, +{ + type Item = OccurrenceValuesRef<'a, T>; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a, T> DoubleEndedIterator for OccurrencesRef<'a, T> +where + Self: 'a, +{ + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl<'a, T> ExactSizeIterator for OccurrencesRef<'a, T> where Self: 'a {} +impl<'a, T> Default for OccurrencesRef<'a, T> { + fn default() -> Self { + static EMPTY: [Vec; 0] = []; + OccurrencesRef { + iter: EMPTY[..].iter().map(|_| unreachable!()), + } + } +} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct OccurrenceValuesRef<'a, T> { + #[allow(clippy::type_complexity)] + iter: Map, fn(&AnyValue) -> &T>, +} + +impl<'a, T> Iterator for OccurrenceValuesRef<'a, T> +where + Self: 'a, +{ + type Item = &'a T; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a, T> DoubleEndedIterator for OccurrenceValuesRef<'a, T> +where + Self: 'a, +{ + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl<'a, T> ExactSizeIterator for OccurrenceValuesRef<'a, T> where Self: 'a {} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct RawOccurrences<'a> { + #[allow(clippy::type_complexity)] + iter: Map>, fn(&Vec) -> RawOccurrenceValues<'_>>, +} + +impl<'a> Iterator for RawOccurrences<'a> { + type Item = RawOccurrenceValues<'a>; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a> DoubleEndedIterator for RawOccurrences<'a> { + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl<'a> ExactSizeIterator for RawOccurrences<'a> {} + +impl<'a> Default for RawOccurrences<'a> { + fn default() -> Self { + static EMPTY: [Vec; 0] = []; + RawOccurrences { + iter: EMPTY[..].iter().map(|_| unreachable!()), + } + } +} + +#[derive(Clone)] +#[allow(missing_debug_implementations)] +pub struct RawOccurrenceValues<'a> { + #[allow(clippy::type_complexity)] + iter: Map, fn(&OsString) -> &OsStr>, +} + +impl<'a> Iterator for RawOccurrenceValues<'a> +where + Self: 'a, +{ + type Item = &'a OsStr; + + fn next(&mut self) -> Option { + self.iter.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a> DoubleEndedIterator for RawOccurrenceValues<'a> +where + Self: 'a, +{ + fn next_back(&mut self) -> Option { + self.iter.next_back() + } +} + +impl<'a> ExactSizeIterator for RawOccurrenceValues<'a> {} + /// Iterate over indices for where an argument appeared when parsing, via [`ArgMatches::indices_of`] /// /// # Examples @@ -1484,6 +1887,16 @@ fn unwrap_string(value: &AnyValue) -> &str { } } +#[track_caller] +fn unwrap_downcast_ref(value: &AnyValue) -> &T { + value.downcast_ref().expect(INTERNAL_ERROR_MSG) +} + +#[track_caller] +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/main.rs b/tests/builder/main.rs index 858f287549c6..22fdd5f0461c 100644 --- a/tests/builder/main.rs +++ b/tests/builder/main.rs @@ -24,7 +24,6 @@ mod error; mod flag_subcommands; mod flags; mod global_args; -mod grouped_values; mod groups; mod help; mod help_env; @@ -33,6 +32,7 @@ mod ignore_errors; mod indices; mod multiple_occurrences; mod multiple_values; +mod occurrences; mod opts; mod positionals; mod posix_compatible; diff --git a/tests/builder/grouped_values.rs b/tests/builder/occurrences.rs similarity index 88% rename from tests/builder/grouped_values.rs rename to tests/builder/occurrences.rs index 726eac929aa3..56716cb7d2f5 100644 --- a/tests/builder/grouped_values.rs +++ b/tests/builder/occurrences.rs @@ -1,6 +1,13 @@ #![cfg(feature = "unstable-grouped")] -use clap::{Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command}; + +fn occurrences_as_vec_vec<'a>(m: &'a ArgMatches, name: &str) -> Vec> { + m.get_occurrences(name) + .unwrap() + .map(Iterator::collect) + .collect() +} #[test] fn grouped_value_works() { @@ -22,7 +29,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 = occurrences_as_vec_vec(&m, "option"); assert_eq!( grouped_vals, vec![ @@ -50,7 +57,7 @@ fn issue_1026() { "target3", "file8", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("target").unwrap().collect(); + let grouped_vals = occurrences_as_vec_vec(&m, "target"); assert_eq!( grouped_vals, vec![ @@ -80,7 +87,7 @@ fn grouped_value_long_flag_delimiter() { "alice,bob", ]) .unwrap(); - let grouped_vals: Vec<_> = m.grouped_values_of("option").unwrap().collect(); + let grouped_vals = occurrences_as_vec_vec(&m, "option"); assert_eq!( grouped_vals, vec![ @@ -104,7 +111,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 = occurrences_as_vec_vec(&m, "option"); assert_eq!( grouped_vals, vec![vec!["foo"], vec!["val1", "val2", "val3"], vec!["bar"]] @@ -124,7 +131,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 = occurrences_as_vec_vec(&m, "pos"); assert_eq!( grouped_vals, vec![vec!["val1", "val2", "val3", "val4", "val5", "val6"]] @@ -145,7 +152,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 = occurrences_as_vec_vec(&m, "pos2"); assert_eq!( grouped_vals, vec![vec!["val2", "val3", "val4", "val5", "val6"]] @@ -167,7 +174,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 = occurrences_as_vec_vec(&m, "pos2"); assert_eq!( grouped_vals, vec![vec!["val2", "val3", "val4", "val5", "val6"]] @@ -190,10 +197,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 = occurrences_as_vec_vec(&m, "pos"); assert_eq!(pos, vec![vec!["1", "2"], vec!["3"], vec!["4"]]); - let flag: Vec<_> = m.grouped_values_of("flag").unwrap().collect(); + let flag = occurrences_as_vec_vec(&m, "flag"); assert_eq!(flag, vec![vec!["a"], vec!["b"]]); } @@ -213,10 +220,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 = occurrences_as_vec_vec(&m, "pos"); assert_eq!(pos, vec![vec!["1", "2"], vec!["3"], vec!["4"]]); - let flag: Vec<_> = m.grouped_values_of("flag").unwrap().collect(); + let flag = occurrences_as_vec_vec(&m, "flag"); assert_eq!(flag, vec![vec!["a"], vec!["b"]]); }