From 69e1e431e9d5c13f6dde97799d25fbe1f3be7be8 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 12 Aug 2022 17:05:02 -0500 Subject: [PATCH 1/4] fix: Publicly expose 'Values' iterator --- src/parser/matches/arg_matches.rs | 18 +++++++++--------- src/parser/matches/mod.rs | 1 + src/parser/mod.rs | 1 + 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 7b77a54500e..6c61e775ad2 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -269,7 +269,7 @@ impl ArgMatches { pub fn remove_many( &mut self, id: &str, - ) -> Option> { + ) -> Option> { MatchesError::unwrap(id, self.try_remove_many(id)) } @@ -916,14 +916,14 @@ impl ArgMatches { pub fn try_remove_many( &mut self, id: &str, - ) -> Result>, MatchesError> { + ) -> Result>, MatchesError> { let arg = match self.try_remove_arg_t::(id)? { Some(arg) => arg, None => return Ok(None), }; let len = arg.num_vals(); let values = arg.into_vals_flatten(); - let values = Values2 { + let values = Values { // enforced by `try_get_arg_t` iter: values.map(|v| v.downcast_into::().expect(INTERNAL_ERROR_MSG)), len, @@ -1086,13 +1086,13 @@ pub(crate) struct SubCommand { /// assert_eq!(values.next(), None); /// ``` #[derive(Clone, Debug)] -pub struct Values2 { +pub struct Values { #[allow(clippy::type_complexity)] iter: Map>>, fn(AnyValue) -> T>, len: usize, } -impl Iterator for Values2 { +impl Iterator for Values { type Item = T; fn next(&mut self) -> Option { @@ -1103,19 +1103,19 @@ impl Iterator for Values2 { } } -impl DoubleEndedIterator for Values2 { +impl DoubleEndedIterator for Values { fn next_back(&mut self) -> Option { self.iter.next_back() } } -impl ExactSizeIterator for Values2 {} +impl ExactSizeIterator for Values {} /// Creates an empty iterator. -impl Default for Values2 { +impl Default for Values { fn default() -> Self { let empty: Vec> = Default::default(); - Values2 { + Values { iter: empty.into_iter().flatten().map(|_| unreachable!()), len: 0, } diff --git a/src/parser/matches/mod.rs b/src/parser/matches/mod.rs index 5fcc088189f..6e06a1eeb75 100644 --- a/src/parser/matches/mod.rs +++ b/src/parser/matches/mod.rs @@ -5,6 +5,7 @@ mod value_source; pub use any_value::AnyValueId; pub use arg_matches::RawValues; +pub use arg_matches::Values; pub use arg_matches::ValuesRef; pub use arg_matches::{ArgMatches, Indices}; pub use value_source::ValueSource; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e8a256c682..efc3d6bf9c5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20,6 +20,7 @@ pub(crate) use self::validator::get_possible_values_cli; pub(crate) use self::validator::Validator; pub use self::matches::RawValues; +pub use self::matches::Values; pub use self::matches::ValuesRef; pub use self::matches::{ArgMatches, Indices, ValueSource}; pub use error::MatchesError; From c45bd6494184835ca79545e519721a0586eb6c01 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 15 Aug 2022 09:47:49 -0500 Subject: [PATCH 2/4] test(builder): Clear up test files names --- tests/builder/{arg_matcher_assertions.rs => arg_matches.rs} | 0 tests/builder/main.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/builder/{arg_matcher_assertions.rs => arg_matches.rs} (100%) diff --git a/tests/builder/arg_matcher_assertions.rs b/tests/builder/arg_matches.rs similarity index 100% rename from tests/builder/arg_matcher_assertions.rs rename to tests/builder/arg_matches.rs diff --git a/tests/builder/main.rs b/tests/builder/main.rs index b13850d6fd5..7912eb90de0 100644 --- a/tests/builder/main.rs +++ b/tests/builder/main.rs @@ -4,7 +4,7 @@ mod action; mod app_settings; mod arg_aliases; mod arg_aliases_short; -mod arg_matcher_assertions; +mod arg_matches; mod borrowed; mod cargo; mod command; From 7486a0b4b9fb131f19bb32fb3dddfb5708aa617d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 15 Aug 2022 10:00:42 -0500 Subject: [PATCH 3/4] feat(parser): Report what arg ids are present For now, we are focusing only on iterating over the argument ids and not the values. This provides a building block for more obscure use cases like iterating over argument values, in order. We are not providing it out of the box at the moment both to not overly incentize a less common case, because it would abstract away a performance hit, and because we want to let people experiment with this and if a common path emerges we can consider it then if there is enough users. Fixes #1206 --- src/parser/matches/arg_matches.rs | 75 +++++++++++++++++++++++++++++++ src/parser/matches/mod.rs | 1 + src/parser/mod.rs | 1 + tests/builder/arg_matches.rs | 67 ++++++++++++++++++++++++++- 4 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/parser/matches/arg_matches.rs b/src/parser/matches/arg_matches.rs index 6c61e775ad2..0742a4aa185 100644 --- a/src/parser/matches/arg_matches.rs +++ b/src/parser/matches/arg_matches.rs @@ -303,6 +303,35 @@ impl ArgMatches { MatchesError::unwrap(id, self.try_contains_id(id)) } + /// Iterate over [`Arg`][crate::Arg] and [`ArgGroup`][crate::ArgGroup] [`Id`][crate::Id]s via [`ArgMatches::ids`]. + /// + /// # Examples + /// + /// ``` + /// # use clap::{Command, arg, value_parser}; + /// + /// let m = Command::new("myprog") + /// .arg(arg!(--color ) + /// .value_parser(["auto", "always", "never"]) + /// .required(false)) + /// .arg(arg!(--config ) + /// .value_parser(value_parser!(std::path::PathBuf)) + /// .required(false)) + /// .get_matches_from(["myprog", "--config=config.toml", "--color=auto"]); + /// assert_eq!(m.ids().len(), 2); + /// assert_eq!( + /// m.ids() + /// .map(|id| id.as_str()) + /// .collect::>(), + /// ["config", "color"] + /// ); + /// ``` + pub fn ids(&self) -> IdsRef<'_> { + IdsRef { + iter: self.args.keys(), + } + } + /// Check if any args were present on the command line /// /// # Examples @@ -1066,6 +1095,52 @@ pub(crate) struct SubCommand { pub(crate) matches: ArgMatches, } +/// Iterate over [`Arg`][crate::Arg] and [`ArgGroup`][crate::ArgGroup] [`Id`][crate::Id]s via [`ArgMatches::ids`]. +/// +/// # Examples +/// +/// ``` +/// # use clap::{Command, arg, value_parser}; +/// +/// let m = Command::new("myprog") +/// .arg(arg!(--color ) +/// .value_parser(["auto", "always", "never"]) +/// .required(false)) +/// .arg(arg!(--config ) +/// .value_parser(value_parser!(std::path::PathBuf)) +/// .required(false)) +/// .get_matches_from(["myprog", "--config=config.toml", "--color=auto"]); +/// assert_eq!( +/// m.ids() +/// .map(|id| id.as_str()) +/// .collect::>(), +/// ["config", "color"] +/// ); +/// ``` +#[derive(Clone, Debug)] +pub struct IdsRef<'a> { + iter: std::slice::Iter<'a, Id>, +} + +impl<'a> Iterator for IdsRef<'a> { + type Item = &'a Id; + + fn next(&mut self) -> Option<&'a Id> { + self.iter.next() + } + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl<'a> DoubleEndedIterator for IdsRef<'a> { + fn next_back(&mut self) -> Option<&'a Id> { + self.iter.next_back() + } +} + +impl<'a> ExactSizeIterator for IdsRef<'a> {} + /// Iterate over multiple values for an argument via [`ArgMatches::remove_many`]. /// /// # Examples diff --git a/src/parser/matches/mod.rs b/src/parser/matches/mod.rs index 6e06a1eeb75..0e3474fb3fe 100644 --- a/src/parser/matches/mod.rs +++ b/src/parser/matches/mod.rs @@ -4,6 +4,7 @@ mod matched_arg; mod value_source; pub use any_value::AnyValueId; +pub use arg_matches::IdsRef; pub use arg_matches::RawValues; pub use arg_matches::Values; pub use arg_matches::ValuesRef; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index efc3d6bf9c5..c99e74f95d3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -19,6 +19,7 @@ pub(crate) use self::parser::{ParseState, Parser}; pub(crate) use self::validator::get_possible_values_cli; pub(crate) use self::validator::Validator; +pub use self::matches::IdsRef; pub use self::matches::RawValues; pub use self::matches::Values; pub use self::matches::ValuesRef; diff --git a/tests/builder/arg_matches.rs b/tests/builder/arg_matches.rs index 1af4af81569..036d8966679 100644 --- a/tests/builder/arg_matches.rs +++ b/tests/builder/arg_matches.rs @@ -1,5 +1,70 @@ +use clap::{arg, value_parser, Command}; #[cfg(debug_assertions)] -use clap::{Arg, ArgAction, Command}; +use clap::{Arg, ArgAction}; + +#[test] +fn ids() { + let m = Command::new("test") + .arg( + arg!(--color ) + .value_parser(["auto", "always", "never"]) + .required(false), + ) + .arg( + arg!(--config ) + .value_parser(value_parser!(std::path::PathBuf)) + .required(false), + ) + .try_get_matches_from(["test", "--config=config.toml", "--color=auto"]) + .unwrap(); + assert_eq!( + m.ids().map(|id| id.as_str()).collect::>(), + ["config", "color"] + ); + assert_eq!(m.ids().len(), 2); +} + +#[test] +fn ids_ignore_unused() { + let m = Command::new("test") + .arg( + arg!(--color ) + .value_parser(["auto", "always", "never"]) + .required(false), + ) + .arg( + arg!(--config ) + .value_parser(value_parser!(std::path::PathBuf)) + .required(false), + ) + .try_get_matches_from(["test", "--config=config.toml"]) + .unwrap(); + assert_eq!( + m.ids().map(|id| id.as_str()).collect::>(), + ["config"] + ); + assert_eq!(m.ids().len(), 1); +} + +#[test] +fn ids_ignore_overridden() { + let m = Command::new("test") + .arg( + arg!(--color ) + .value_parser(["auto", "always", "never"]) + .required(false), + ) + .arg( + arg!(--config ) + .value_parser(value_parser!(std::path::PathBuf)) + .required(false) + .overrides_with("color"), + ) + .try_get_matches_from(["test", "--config=config.toml", "--color=auto"]) + .unwrap(); + assert_eq!(m.ids().map(|id| id.as_str()).collect::>(), ["color"]); + assert_eq!(m.ids().len(), 1); +} #[test] #[cfg(debug_assertions)] From 9c9cc9fcff1ac8b233597eb1a7d1079d66530352 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 15 Aug 2022 10:40:20 -0500 Subject: [PATCH 4/4] docs(cookbook): Add position-sensitive example --- Cargo.toml | 4 ++ examples/find.md | 47 ++++++++++++++++++++ examples/find.rs | 99 +++++++++++++++++++++++++++++++++++++++++++ src/_cookbook/find.rs | 7 +++ src/_cookbook/mod.rs | 5 +++ 5 files changed, 162 insertions(+) create mode 100644 examples/find.md create mode 100644 examples/find.rs create mode 100644 src/_cookbook/find.rs diff --git a/Cargo.toml b/Cargo.toml index 404f85f28f0..214c230eec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,10 @@ required-features = ["cargo"] name = "escaped-positional-derive" required-features = ["derive"] +[[example]] +name = "find" +required-features = ["cargo"] + [[example]] name = "git-derive" required-features = ["derive"] diff --git a/examples/find.md b/examples/find.md new file mode 100644 index 00000000000..decfa2abce2 --- /dev/null +++ b/examples/find.md @@ -0,0 +1,47 @@ +`find` is an example of position-sensitive flags + +```console +$ find --help +clap 4.0.0-alpha.0 +A simple to use, efficient, and full-featured Command Line Argument Parser + +USAGE: + find[EXE] [OPTIONS] --name + +OPTIONS: + -h, --help Print help information + -V, --version Print version information + +TESTS: + --empty File is empty and is either a regular file or a directory + --name Base of file name (the path with the leading directories removed) matches + shell pattern pattern + +OPERATORS: + -o, --or expr2 is not evaluate if exp1 is true + -a, --and Same as `expr1 expr1` + +$ find --empty -o --name .keep +[ + ( + "empty", + Bool( + true, + ), + ), + ( + "or", + Bool( + true, + ), + ), + ( + "name", + String( + ".keep", + ), + ), +] + +``` + diff --git a/examples/find.rs b/examples/find.rs new file mode 100644 index 00000000000..c16ff991336 --- /dev/null +++ b/examples/find.rs @@ -0,0 +1,99 @@ +use std::collections::BTreeMap; + +use clap::{arg, command, ArgGroup, ArgMatches, Command}; + +fn main() { + let matches = cli().get_matches(); + let values = Value::from_matches(&matches); + println!("{:#?}", values); +} + +fn cli() -> Command<'static> { + command!() + .group(ArgGroup::new("tests").multiple(true)) + .next_help_heading("TESTS") + .args([ + arg!(--empty "File is empty and is either a regular file or a directory").group("tests"), + arg!(--name "Base of file name (the path with the leading directories removed) matches shell pattern pattern").group("tests"), + ]) + .group(ArgGroup::new("operators").multiple(true)) + .next_help_heading("OPERATORS") + .args([ + arg!(-o - -or "expr2 is not evaluate if exp1 is true").group("operators"), + arg!(-a - -and "Same as `expr1 expr1`").group("operators"), + ]) +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum Value { + Bool(bool), + String(String), +} + +impl Value { + pub fn from_matches(matches: &ArgMatches) -> Vec<(clap::Id, Self)> { + let mut values = BTreeMap::new(); + for id in matches.ids() { + if matches.try_get_many::(id.as_str()).is_ok() { + // ignore groups + continue; + } + let value_source = matches + .value_source(id.as_str()) + .expect("id came from matches"); + if value_source != clap::parser::ValueSource::CommandLine { + // Any other source just gets tacked on at the end (like default values) + continue; + } + if Self::extract::(matches, id, &mut values) { + continue; + } + if Self::extract::(matches, id, &mut values) { + continue; + } + unimplemented!("unknown type for {}: {:?}", id, matches); + } + values.into_values().collect::>() + } + + fn extract + Send + Sync + 'static>( + matches: &ArgMatches, + id: &clap::Id, + output: &mut BTreeMap, + ) -> bool { + match matches.try_get_many::(id.as_str()) { + Ok(Some(values)) => { + for (value, index) in values.zip( + matches + .indices_of(id.as_str()) + .expect("id came from matches"), + ) { + output.insert(index, (id.clone(), value.clone().into())); + } + true + } + Ok(None) => { + unreachable!("`ids` only reports what is present") + } + Err(clap::parser::MatchesError::UnknownArgument { .. }) => { + unreachable!("id came from matches") + } + Err(clap::parser::MatchesError::Downcast { .. }) => false, + Err(_) => { + unreachable!("id came from matches") + } + } + } +} + +impl From for Value { + fn from(other: String) -> Self { + Self::String(other) + } +} + +impl From for Value { + fn from(other: bool) -> Self { + Self::Bool(other) + } +} diff --git a/src/_cookbook/find.rs b/src/_cookbook/find.rs new file mode 100644 index 00000000000..2ca11c665d2 --- /dev/null +++ b/src/_cookbook/find.rs @@ -0,0 +1,7 @@ +//! # Example: find-like CLI (Builder API) +//! +//! ```rust +#![doc = include_str!("../../examples/find.rs")] +//! ``` +//! +#![doc = include_str!("../../examples/find.md")] diff --git a/src/_cookbook/mod.rs b/src/_cookbook/mod.rs index 1a78ed11bb7..8c45d2cf0ff 100644 --- a/src/_cookbook/mod.rs +++ b/src/_cookbook/mod.rs @@ -16,6 +16,10 @@ //! - Subcommands //! - Cargo plugins //! +//! find-like interface: [builder][find] +//! - Topics: +//! - Position-sensitive flags +//! //! git-like interface: [builder][git], [derive][git_derive] //! - Topics: //! - Subcommands @@ -46,6 +50,7 @@ pub mod cargo_example; pub mod cargo_example_derive; pub mod escaped_positional; pub mod escaped_positional_derive; +pub mod find; pub mod git; pub mod git_derive; pub mod multicall_busybox;