Skip to content

Commit

Permalink
AppSettings:IgnoreErrors: Allow partial parsing in the presence of er…
Browse files Browse the repository at this point in the history
…rors

clap-rs#1880
  • Loading branch information
kolloch committed May 13, 2021
1 parent 73cf47a commit f4e2688
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 2 deletions.
11 changes: 10 additions & 1 deletion src/build/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2225,7 +2225,13 @@ impl<'help> App<'help> {

// do the real parsing
let mut parser = Parser::new(self);
parser.get_matches_with(&mut matcher, it)?;
if let Err(error) = parser.get_matches_with(&mut matcher, it) {
if self.is_set(AppSettings::IgnoreErrors) {
debug!("ignoring error: {}", error);
} else {
return Err(error)
}
}

let global_arg_vec: Vec<Id> = self.get_used_global_args(&matcher);

Expand Down Expand Up @@ -2376,6 +2382,9 @@ impl<'help> App<'help> {
$sc.set(AppSettings::GlobalVersion);
$sc.version = Some($_self.version.unwrap());
}
if $_self.settings.is_set(AppSettings::IgnoreErrors) {
// $sc.set(AppSettings::PartialParsing);
}
$sc.settings = $sc.settings | $_self.g_settings;
$sc.g_settings = $sc.g_settings | $_self.g_settings;
$sc.term_w = $_self.term_w;
Expand Down
37 changes: 37 additions & 0 deletions src/build/app/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ bitflags! {
const SUBCOMMAND_PRECEDENCE_OVER_ARG = 1 << 41;
const DISABLE_HELP_FLAG = 1 << 42;
const USE_LONG_FORMAT_FOR_HELP_SC = 1 << 43;
const IGNORE_ERRORS = 1 << 44;
}
}

Expand Down Expand Up @@ -135,6 +136,8 @@ impl_settings! { AppSettings, AppFlags,
=> Flags::UNIFIED_HELP,
NextLineHelp("nextlinehelp")
=> Flags::NEXT_LINE_HELP,
IgnoreErrors("ignoreerrors")
=> Flags::IGNORE_ERRORS,
DisableVersionForSubcommands("disableversionforsubcommands")
=> Flags::DISABLE_VERSION_FOR_SC,
WaitOnError("waitonerror")
Expand Down Expand Up @@ -841,6 +844,40 @@ pub enum AppSettings {
/// ```
NextLineHelp,

/// Try not to fail on parse errors like missing option values. This is a
/// global option that gets propagated sub commands.
///
/// Issue: [#1880 Partial / Pre Parsing a
/// CLI](https://github.com/clap-rs/clap/issues/1880)
///
/// This is the basis for:
///
/// * [Changing app settings based on
/// flags](https://github.com/clap-rs/clap/issues/1880#issuecomment-637779787)
/// * [#1232 Dynamic completion
/// support](https://github.com/clap-rs/clap/issues/1232)
///
/// Support is not complete: Errors are still possible but they can be
/// avoided in many cases.
///
/// ```rust
/// # use clap::{App, AppSettings};
/// let app = App::new("app")
/// .setting(AppSettings::IgnoreErrors)
/// .arg("-c, --config=[FILE] 'Sets a custom config file'")
/// .arg("-x, --stuff=[FILE] 'Sets a custom stuff file'")
/// .arg("-f 'Flag'");
///
/// let r = app.try_get_matches_from(vec!["app", "-c", "file", "-f", "-x"]);
///
/// assert!(r.is_ok(), "unexpected error: {:?}", r);
/// let m = r.unwrap();
/// assert_eq!(m.value_of("config"), Some("file"));
/// assert!(m.is_present("f"));
/// assert_eq!(m.value_of("stuff"), None);
/// ```
IgnoreErrors,

/// Allows [``]s to override all requirements of the parent command.
/// For example, if you had a subcommand or top level application with a required argument
/// that is only required as long as there is no subcommand present,
Expand Down
10 changes: 9 additions & 1 deletion src/parse/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,8 @@ impl<'help, 'app> Parser<'help, 'app> {
}
}

let partial_parsing_enabled = self.is_set(AS::IgnoreErrors);

if let Some(sc) = self.app.subcommands.iter_mut().find(|s| s.name == sc_name) {
let mut sc_matcher = ArgMatcher::default();
// Display subcommand name, short and long in usage
Expand Down Expand Up @@ -912,7 +914,13 @@ impl<'help, 'app> Parser<'help, 'app> {
p.cur_idx.set(self.cur_idx.get());
p.skip_idxs = self.skip_idxs;
}
p.get_matches_with(&mut sc_matcher, it)?;
if let Err(error) = p.get_matches_with(&mut sc_matcher, it) {
if partial_parsing_enabled {
debug!("ignored error in subcommand {}: {:?}", sc_name, error);
} else {
return Err(error)
}
}
}
let name = sc.name.clone();
matcher.subcommand(SubCommand {
Expand Down
98 changes: 98 additions & 0 deletions tests/partial_parsing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use clap::{App, AppSettings, Arg};

#[test]
fn single_short_arg_without_value() {
let app = App::new("app")
.setting(AppSettings::IgnoreErrors)
.arg("-c, --config=[FILE] 'Sets a custom config file'");

let r = app.try_get_matches_from(vec![
"app", "-c" /* missing: , "config file" */]);

assert!(r.is_ok(), "unexpected error: {:?}", r);
let m = r.unwrap();
assert!(m.is_present("config"));
}

#[test]
fn single_long_arg_without_value() {
let app = App::new("app")
.setting(AppSettings::IgnoreErrors)
.arg("-c, --config=[FILE] 'Sets a custom config file'");

let r = app.try_get_matches_from(vec![
"app", "--config" /* missing: , "config file" */]);

assert!(r.is_ok(), "unexpected error: {:?}", r);
let m = r.unwrap();
assert!(m.is_present("config"));
}

#[test]
fn multiple_args_and_final_arg_without_value() {
let app = App::new("app")
.setting(AppSettings::IgnoreErrors)
.arg("-c, --config=[FILE] 'Sets a custom config file'")
.arg("-x, --stuff=[FILE] 'Sets a custom stuff file'")
.arg("-f 'Flag'");

let r = app.try_get_matches_from(vec![
"app", "-c", "file", "-f", "-x" /* missing: , "some stuff" */ ]);

assert!(r.is_ok(), "unexpected error: {:?}", r);
let m = r.unwrap();
assert_eq!(m.value_of("config"), Some("file"));
assert!(m.is_present("f"));
assert_eq!(m.value_of("stuff"), None);
}

#[test]
fn multiple_args_and_intermittent_arg_without_value() {
let app = App::new("app")
.setting(AppSettings::IgnoreErrors)
.arg("-c, --config=[FILE] 'Sets a custom config file'")
.arg("-x, --stuff=[FILE] 'Sets a custom stuff file'")
.arg("-f 'Flag'");

let r = app.try_get_matches_from(vec![
"app", "-x" /* missing: ,"some stuff" */, "-c", "file", "-f"]);

assert!(r.is_ok(), "unexpected error: {:?}", r);
let m = r.unwrap();
assert_eq!(m.value_of("config"), Some("file"));
assert!(m.is_present("f"));
assert_eq!(m.value_of("stuff"), None);
}

#[test]
fn subcommand() {
let app = App::new("test")
.setting(AppSettings::IgnoreErrors)
.subcommand(
App::new("some")
.arg(
Arg::new("test")
.short('t')
.long("test")
.takes_value(true)
.about("testing testing"),
)
.arg(
Arg::new("stuff")
.short('x')
.long("stuff")
.takes_value(true)
.about("stuf value"),
),
)
.arg(Arg::new("other").long("other"));

let m = app.get_matches_from(vec![
"myprog", "some", "--test" /* missing: ,"some val" */, "-x", "some other val"]);

assert_eq!(m.subcommand_name().unwrap(), "some");
let sub_m = m.subcommand_matches("some").unwrap();
assert!(sub_m.is_present("test"), "expected subcommand to be present due to partial parsing");
assert_eq!(sub_m.value_of("test"), None);
assert_eq!(sub_m.value_of("stuff"), Some("some other val"));
}

0 comments on commit f4e2688

Please sign in to comment.