From 834f600a6fb4ce17d9a381ebc6acef403e5abd3a Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 5 Jun 2021 13:18:06 +0200 Subject: [PATCH 1/2] infer long arguments --- src/build/app/settings.rs | 13 ++++++++++++- src/parse/parser.rs | 40 ++++++++++++++++++++++++++++++++++++--- tests/opts.rs | 32 ++++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/build/app/settings.rs b/src/build/app/settings.rs index 3e0c1662..2c60a5f6 100644 --- a/src/build/app/settings.rs +++ b/src/build/app/settings.rs @@ -50,6 +50,7 @@ bitflags! { const DISABLE_HELP_FLAG = 1 << 42; const USE_LONG_FORMAT_FOR_HELP_SC = 1 << 43; const DISABLE_ENV = 1 << 44; + const INFER_LONG_ARGS = 1 << 45; } } @@ -153,7 +154,9 @@ impl_settings! { AppSettings, AppFlags, InferSubcommands("infersubcommands") => Flags::INFER_SUBCOMMANDS, AllArgsOverrideSelf("allargsoverrideself") - => Flags::ARGS_OVERRIDE_SELF + => Flags::ARGS_OVERRIDE_SELF, + InferLongArgs("inferlongargs") + => Flags::INFER_LONG_ARGS } /// Application level settings, which affect how [`App`] operates @@ -826,6 +829,14 @@ pub enum AppSettings { /// [aliases]: App::alias() InferSubcommands, + /// Tries to match unknown args to partial long arguments or their [aliases]. For example, to + /// match an argument named `--test`, one could use `--t`, `--te`, `--tes`, and `--test`. + /// + /// **NOTE:** The match *must not* be ambiguous at all in order to succeed. i.e. to match + /// `--te` to `--test` there could not also be a subcommand or alias `--temp` because both + /// start with `--te` + InferLongArgs, + /// Specifies that the parser should not assume the first argument passed is the binary name. /// This is normally the case when using a "daemon" style mode, or an interactive CLI where one /// one would not normally type the binary or program name for each command. diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 29573153..27e1c6f5 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -1041,6 +1041,7 @@ impl<'help, 'app> Parser<'help, 'app> { debug!("No"); (long_arg, None) }; + if let Some(opt) = self.app.args.get(&arg.to_os_string()) { debug!( "Parser::parse_long_arg: Found valid opt or flag '{}'", @@ -1049,12 +1050,45 @@ impl<'help, 'app> Parser<'help, 'app> { self.app.settings.set(AS::ValidArgFound); self.seen.push(opt.id.clone()); if opt.is_set(ArgSettings::TakesValue) { - Ok(self.parse_opt(&val, opt, matcher)?) + return self.parse_opt(&val, opt, matcher); } else { self.check_for_help_and_version_str(&arg)?; - Ok(self.parse_flag(opt, matcher)) + return Ok(self.parse_flag(opt, matcher)); } - } else if let Some(sc_name) = self.possible_long_flag_subcommand(&arg) { + } + + if self.is_set(AS::InferLongArgs) { + let arg_str = arg.to_string_lossy(); + let matches: Vec<_> = self + .app + .args + .args() + .filter(|a| { + a.long + .map_or(false, |long| long.starts_with(arg_str.as_ref())) + || a.aliases + .iter() + .any(|(alias, _)| alias.starts_with(arg_str.as_ref())) + }) + .collect(); + + if let [opt] = matches.as_slice() { + debug!( + "Parser::parse_long_arg: Found valid opt or flag '{}'", + opt.to_string() + ); + self.app.settings.set(AS::ValidArgFound); + self.seen.push(opt.id.clone()); + if opt.is_set(ArgSettings::TakesValue) { + return self.parse_opt(&val, opt, matcher); + } else { + self.check_for_help_and_version_str(&arg)?; + return Ok(self.parse_flag(opt, matcher)); + } + } + } + + if let Some(sc_name) = self.possible_long_flag_subcommand(&arg) { Ok(ParseResult::FlagSubCommand(sc_name.to_string())) } else if self.is_set(AS::AllowLeadingHyphen) { Ok(ParseResult::MaybeHyphenValue) diff --git a/tests/opts.rs b/tests/opts.rs index 0f4aa78b..45a46f56 100644 --- a/tests/opts.rs +++ b/tests/opts.rs @@ -1,6 +1,6 @@ mod utils; -use clap::{App, Arg, ArgMatches, ArgSettings, ErrorKind}; +use clap::{App, AppSettings, Arg, ArgMatches, ArgSettings, ErrorKind}; #[cfg(feature = "suggestions")] static DYM: &str = @@ -577,3 +577,33 @@ fn issue_2279() { assert_eq!(after_help_heading.value_of("foo"), Some("bar")); } + +#[test] +fn infer_long_arg() { + let app = App::new("test") + .setting(AppSettings::InferLongArgs) + .arg(Arg::new("racetrack").long("racetrack").alias("autobahn")) + .arg(Arg::new("racecar").long("racecar").takes_value(true)); + + let matches = app.clone().get_matches_from(&["test", "--racec=hello"]); + assert!(!matches.is_present("racetrack")); + assert_eq!(matches.value_of("racecar"), Some("hello")); + + let matches = app.clone().get_matches_from(&["test", "--racet"]); + assert!(matches.is_present("racetrack")); + assert_eq!(matches.value_of("racecar"), None); + + let matches = app.clone().get_matches_from(&["test", "--auto"]); + assert!(matches.is_present("racetrack")); + assert_eq!(matches.value_of("racecar"), None); + + let app = App::new("test") + .setting(AppSettings::InferLongArgs) + .arg(Arg::new("arg").long("arg")); + + let matches = app.clone().get_matches_from(&["test", "--"]); + assert!(!matches.is_present("arg")); + + let matches = app.clone().get_matches_from(&["test", "--a"]); + assert!(matches.is_present("arg")); +} From 6a574bf73c3379e6ff7f1943f9c6e4aea9b0bca4 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 6 Jun 2021 12:32:32 +0200 Subject: [PATCH 2/2] Fix documentation for InferLongArgs Co-authored-by: Pavan Kumar Sunkara --- src/build/app/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/app/settings.rs b/src/build/app/settings.rs index 2c60a5f6..04bfc286 100644 --- a/src/build/app/settings.rs +++ b/src/build/app/settings.rs @@ -833,7 +833,7 @@ pub enum AppSettings { /// match an argument named `--test`, one could use `--t`, `--te`, `--tes`, and `--test`. /// /// **NOTE:** The match *must not* be ambiguous at all in order to succeed. i.e. to match - /// `--te` to `--test` there could not also be a subcommand or alias `--temp` because both + /// `--te` to `--test` there could not also be another argument or alias `--temp` because both /// start with `--te` InferLongArgs,