Skip to content

Commit

Permalink
feat: Implement Arg::required_if_eq_all
Browse files Browse the repository at this point in the history
  • Loading branch information
omar25h committed Feb 27, 2021
1 parent c9e875e commit 59c9c44
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 3 deletions.
91 changes: 90 additions & 1 deletion src/build/arg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,7 @@ impl<'help> Arg<'help> {

/// Allows specifying that this argument is [required] based on multiple conditions. The
/// conditions are set up in a `(arg, val)` style tuple. The requirement will only become valid
/// if one of the specified `arg`'s value equals it's corresponding `val`.
/// if one of the specified `arg`'s value equals its corresponding `val`.
///
/// **NOTE:** If using YAML the values should be laid out as follows
///
Expand Down Expand Up @@ -1520,6 +1520,90 @@ impl<'help> Arg<'help> {
self
}

/// Allows specifying that this argument is [required] based on multiple conditions. The
/// conditions are set up in a `(arg, val)` style tuple. The requirement will only become valid
/// if every one of the specified `arg`'s value equals its corresponding `val`.
///
/// **NOTE:** If using YAML the values should be laid out as follows
///
/// ```yaml
/// required_if_eq_all:
/// - [arg, val]
/// - [arg2, val2]
/// ```
///
/// # Examples
///
/// ```rust
/// # use clap::Arg;
/// Arg::new("config")
/// .required_if_eq_all(&[
/// ("extra", "val"),
/// ("option", "spec")
/// ])
/// # ;
/// ```
///
/// Setting `Arg::required_if_eq_all(&[(arg, val)])` makes this arg required if all of the `arg`s
/// are used at runtime and every value is equal to its corresponding `val`. If the `arg`'s value is
/// anything other than `val`, this argument isn't required.
///
/// ```rust
/// # use clap::{App, Arg};
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .required_if_eq_all(&[
/// ("extra", "val"),
/// ("option", "spec")
/// ])
/// .takes_value(true)
/// .long("config"))
/// .arg(Arg::new("extra")
/// .takes_value(true)
/// .long("extra"))
/// .arg(Arg::new("option")
/// .takes_value(true)
/// .long("option"))
/// .try_get_matches_from(vec![
/// "prog", "--option", "spec"
/// ]);
///
/// assert!(res.is_ok()); // We didn't use --option=spec --extra=val so "cfg" isn't required
/// ```
///
/// Setting `Arg::required_if_eq_all(&[(arg, val)])` and having all of the `arg`s used with its
/// value of `val` but *not* using this arg is an error.
///
/// ```rust
/// # use clap::{App, Arg, ErrorKind};
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .required_if_eq_all(&[
/// ("extra", "val"),
/// ("option", "spec")
/// ])
/// .takes_value(true)
/// .long("config"))
/// .arg(Arg::new("extra")
/// .takes_value(true)
/// .long("extra"))
/// .arg(Arg::new("option")
/// .takes_value(true)
/// .long("option"))
/// .try_get_matches_from(vec![
/// "prog", "--extra", "val", "--option", "spec"
/// ]);
///
/// assert!(res.is_err());
/// assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
/// ```
/// [required]: ./struct.Arg.html#method.required
pub fn required_if_eq_all<T: Key>(mut self, ifs: &[(T, &'help str)]) -> Self {
self.r_ifs
.extend(ifs.into_iter().map(|(id, val)| (Id::from_ref(id), *val)));
self.setting(ArgSettings::RequiredAll)
}

/// Sets multiple arguments by names that are required when this one is present I.e. when
/// using this argument, the following arguments *must* be present.
///
Expand Down Expand Up @@ -4537,6 +4621,11 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
"required" => yaml_to_bool!(a, v, required),
"required_if_eq" => yaml_tuple2!(a, v, required_if_eq),
"required_if_eq_any" => yaml_tuple2!(a, v, required_if_eq),
"required_if_eq_all" => {
a = yaml_tuple2!(a, v, required_if_eq);
a.settings.set(ArgSettings::RequiredAll);
a
},
"takes_value" => yaml_to_bool!(a, v, takes_value),
"index" => yaml_to_usize!(a, v, index),
"global" => yaml_to_bool!(a, v, global),
Expand Down
8 changes: 8 additions & 0 deletions src/build/arg/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ bitflags! {
const HIDDEN_LONG_H = 1 << 19;
const MULTIPLE_VALS = 1 << 20 | Self::TAKES_VAL.bits;
const HIDE_ENV = 1 << 21 | Self::TAKES_VAL.bits;
const R_ALL = 1 << 22;
}
}

Expand All @@ -45,6 +46,7 @@ impl_settings! { ArgSettings, ArgFlags,
UseValueDelimiter("usevaluedelimiter") => Flags::USE_DELIM,
NextLineHelp("nextlinehelp") => Flags::NEXT_LINE_HELP,
RequiredUnlessAll("requiredunlessall") => Flags::R_UNLESS_ALL,
RequiredAll("requiredall") => Flags::R_ALL,
RequireDelimiter("requiredelimiter") => Flags::REQ_DELIM,
HidePossibleValues("hidepossiblevalues") => Flags::HIDE_POS_VALS,
AllowHyphenValues("allowhyphenvalues") => Flags::ALLOW_TAC_VALS,
Expand Down Expand Up @@ -117,6 +119,8 @@ pub enum ArgSettings {
HiddenLongHelp,
#[doc(hidden)]
RequiredUnlessAll,
#[doc(hidden)]
RequiredAll,
}

#[cfg(test)]
Expand Down Expand Up @@ -145,6 +149,10 @@ mod test {
"nextlinehelp".parse::<ArgSettings>().unwrap(),
ArgSettings::NextLineHelp
);
assert_eq!(
"requiredall".parse::<ArgSettings>().unwrap(),
ArgSettings::RequiredAll
);
assert_eq!(
"requiredunlessall".parse::<ArgSettings>().unwrap(),
ArgSettings::RequiredUnlessAll
Expand Down
17 changes: 15 additions & 2 deletions src/parse/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,13 +561,26 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {

// Validate the conditionally required args
for a in self.p.app.args.args() {
let mut match_any = false;
let mut match_all = true;

for (other, val) in &a.r_ifs {
if let Some(ma) = matcher.get(other) {
if ma.contains_val(val) && !matcher.contains(&a.id) {
return self.missing_required_error(matcher, vec![a.id.clone()]);
if ma.contains_val(val) {
match_any = true;
} else {
match_all = false;
}
} else {
match_all = false;
}
}

let is_arg_required = if a.is_set(ArgSettings::RequiredAll) { match_all } else { match_any };

if is_arg_required && !matcher.contains(&a.id) {
return self.missing_required_error(matcher, vec![a.id.clone()])
}
}
Ok(())
}
Expand Down
33 changes: 33 additions & 0 deletions tests/require.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,39 @@ fn required_if_val_present_fail() {
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
}

#[test]
fn required_if_all_values_present_pass() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec!["ri", "--extra", "val", "--option", "spec", "--config", "my.cfg"]);

assert!(res.is_ok());
}

#[test]
fn required_if_all_values_present_fail() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec!["ri", "--extra", "val", "--option", "spec"]);

assert!(res.is_err());
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
}

#[test]
fn list_correct_required_args() {
let app = App::new("Test app")
Expand Down

0 comments on commit 59c9c44

Please sign in to comment.