Skip to content

Commit

Permalink
setting: IgnoreErrors - Allow parsing despite missing option values
Browse files Browse the repository at this point in the history
Implemented as AppSetting::Ignore errors as suggested by
@CreepySkeleton in
clap-rs#1880 (comment).

This is not a complete implementation but it works already in
surprisingly many situations.

clap-rs#1880
  • Loading branch information
kolloch committed May 13, 2021
1 parent 73cf47a commit 54be4e9
Show file tree
Hide file tree
Showing 4 changed files with 163 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
107 changes: 107 additions & 0 deletions tests/partial_parsing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 54be4e9

Please sign in to comment.