Skip to content

Commit

Permalink
feat(Conditional Requirements): adds the ability to conditionally req…
Browse files Browse the repository at this point in the history
…uire additional args

An arg can now conditionally require additional arguments if it's value matches a specific value.

For example, arg A can state that it only requires arg B if the value X was used with arg A. If any
other value is used with A, arg B isn't required.

Relates to #764
  • Loading branch information
kbknapp committed Dec 29, 2016
1 parent eca6091 commit 198449d
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 45 deletions.
33 changes: 22 additions & 11 deletions src/app/macros.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
macro_rules! remove_overriden {
(@remove $_self:ident, $v:ident, $a:ident.$ov:ident) => {
if let Some(ref ora) = $a.$ov() {
vec_remove_all!($_self.$v, ora);
(@remove_requires $rem_from:expr, $a:ident.$ov:ident) => {
if let Some(ora) = $a.$ov() {
for i in (0 .. $rem_from.len()).rev() {
let should_remove = ora.iter().any(|&(_, ref name)| name == &$rem_from[i]);
if should_remove { $rem_from.swap_remove(i); }
}
}
};
(@remove $rem_from:expr, $a:ident.$ov:ident) => {
if let Some(ora) = $a.$ov() {
vec_remove_all!($rem_from, ora.iter());
}
};
(@arg $_self:ident, $arg:ident) => {
remove_overriden!(@remove $_self, required, $arg.requires);
remove_overriden!(@remove $_self, blacklist, $arg.blacklist);
remove_overriden!(@remove $_self, overrides, $arg.overrides);
remove_overriden!(@remove_requires $_self.required, $arg.requires);
remove_overriden!(@remove $_self.blacklist, $arg.blacklist);
remove_overriden!(@remove $_self.overrides, $arg.overrides);
};
($_self:ident, $name:expr) => {
debugln!("macro=remove_overriden!;");
Expand Down Expand Up @@ -41,7 +49,7 @@ macro_rules! arg_post_processing {
$matcher.remove_all(or);
for pa in or { remove_overriden!($me, pa); }
$me.overrides.extend(or);
vec_remove_all!($me.required, or);
vec_remove_all!($me.required, or.iter());
} else { sdebugln!("No"); }

// Handle conflicts
Expand All @@ -62,15 +70,15 @@ macro_rules! arg_post_processing {
}

$me.blacklist.extend(bl);
vec_remove_all!($me.overrides, bl);
vec_remove_all!($me.required, bl);
vec_remove_all!($me.overrides, bl.iter());
vec_remove_all!($me.required, bl.iter());
} else { sdebugln!("No"); }

// Add all required args which aren't already found in matcher to the master
// list
debug!("Does '{}' have requirements...", $arg.to_string());
if let Some(reqs) = $arg.requires() {
for n in reqs {
for n in reqs.iter().filter(|&&(val, _)| val.is_none()).map(|&(_, name)| name) {
if $matcher.contains(&n) {
sdebugln!("\tYes '{}' but it's already met", n);
continue;
Expand Down Expand Up @@ -103,7 +111,10 @@ macro_rules! _handle_group_reqs{
};
debugln!("iter;grp={};found={:?}", grp.name, found);
if found {
vec_remove_all!($me.required, &grp.args);
for i in (0 .. $me.required.len()).rev() {
let should_remove = grp.args.contains(&grp.args[i]);
if should_remove { $me.required.swap_remove(i); }
}
debugln!("Adding args from group to blacklist...{:?}", grp.args);
if !grp.multiple {
$me.blacklist.extend(&grp.args);
Expand Down
2 changes: 1 addition & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for App<'n, 'e> {
fn id(&self) -> usize { self.p.id }
fn kind(&self) -> ArgKind { ArgKind::Subcmd }
fn overrides(&self) -> Option<&[&'e str]> { None }
fn requires(&self) -> Option<&[&'e str]> { None }
fn requires(&self) -> Option<&[(Option<&'e str>, &'n str)]> { None }
fn blacklist(&self) -> Option<&[&'e str]> { None }
fn required_unless(&self) -> Option<&[&'e str]> { None }
fn val_names(&self) -> Option<&VecMap<&'e str>> { None }
Expand Down
63 changes: 48 additions & 15 deletions src/app/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use suggestions;
pub struct Parser<'a, 'b>
where 'a: 'b
{
required: Vec<&'b str>,
required: Vec<&'a str>,
pub short_list: Vec<char>,
pub long_list: Vec<&'b str>,
blacklist: Vec<&'b str>,
Expand Down Expand Up @@ -326,13 +326,13 @@ impl<'a, 'b> Parser<'a, 'b>
for a in &$v1 {
if let Some(a) = self.$t1.$i1().filter(|arg| &arg.b.name == a).next() {
if let Some(ref rl) = a.b.requires {
for r in rl {
if !reqs.contains(r) {
if $_self.$t1.$i1().any(|t| &t.b.name == r) {
$tmp.push(*r);
} else if $_self.$t2.$i2().any(|t| &t.b.name == r) {
for &(_, r) in rl.iter() {
if !reqs.contains(&r) {
if $_self.$t1.$i1().any(|t| &t.b.name == &r) {
$tmp.push(r);
} else if $_self.$t2.$i2().any(|t| &t.b.name == &r) {
$v2.push(r);
} else if $_self.$t3.$i3().any(|t| &t.b.name == r) {
} else if $_self.$t3.$i3().any(|t| &t.b.name == &r) {
$v3.push(r);
} else if $_self.groups.contains_key(r) {
$gv.push(r);
Expand Down Expand Up @@ -918,6 +918,7 @@ impl<'a, 'b> Parser<'a, 'b>
.expect(INTERNAL_ERROR_MSG);
try!(self.parse_subcommand(sc_name, matcher, it));
};
try!(self.parse_subcommand(sc_name, matcher, it));
} else if self.is_set(AppSettings::SubcommandRequired) {
let bn = self.meta.bin_name.as_ref().unwrap_or(&self.meta.name);
return Err(Error::missing_subcommand(bn,
Expand Down Expand Up @@ -1603,23 +1604,28 @@ impl<'a, 'b> Parser<'a, 'b>
Ok(())
}

fn validate_num_args(&self, matcher: &mut ArgMatcher) -> ClapResult<()> {
debugln!("fn=validate_num_args;");
fn validate_matched_args(&self, matcher: &mut ArgMatcher) -> ClapResult<()> {
debugln!("fn=validate_matched_args;");
for (name, ma) in matcher.iter() {
debugln!("iter;name={}", name);
if let Some(opt) = find_by_name!(self, name, opts, iter) {
try!(self._validate_num_vals(opt, ma, matcher));
try!(self.validate_arg_num_vals(opt, ma, matcher));
try!(self.validate_arg_requires(opt, ma, matcher));
} else if let Some(flag) = find_by_name!(self, name, flags, iter) {
// Only requires
try!(self.validate_arg_requires(flag, ma, matcher));
} else if let Some(pos) = find_by_name!(self, name, positionals, values) {
try!(self._validate_num_vals(pos, ma, matcher));
try!(self.validate_arg_num_vals(pos, ma, matcher));
try!(self.validate_arg_requires(pos, ma, matcher));
}
}
Ok(())
}

fn _validate_num_vals<A>(&self, a: &A, ma: &MatchedArg, matcher: &ArgMatcher) -> ClapResult<()>
fn validate_arg_num_vals<A>(&self, a: &A, ma: &MatchedArg, matcher: &ArgMatcher) -> ClapResult<()>
where A: AnyArg<'a, 'b> + Display
{
debugln!("fn=_validate_num_vals;");
debugln!("fn=validate_arg_num_vals;");
if let Some(num) = a.num_vals() {
debugln!("num_vals set: {}", num);
let should_err = if a.is_set(ArgSettings::Multiple) {
Expand Down Expand Up @@ -1679,6 +1685,32 @@ impl<'a, 'b> Parser<'a, 'b>
Ok(())
}

fn validate_arg_requires<A>(&self, a: &A, ma: &MatchedArg, matcher: &ArgMatcher) -> ClapResult<()>
where A: AnyArg<'a, 'b> + Display
{
debugln!("fn=validate_arg_requires;");
if let Some(a_reqs) = a.requires() {
for &(val, name) in a_reqs.iter().filter(|&&(val, _)| val.is_some()) {
if ma.vals.values().any(|v| v == val.expect(INTERNAL_ERROR_MSG)) {
if matcher.contains(name) { continue; }

let mut reqs = self.required.iter().map(|&r| &*r).collect::<Vec<_>>();
reqs.retain(|n| !matcher.contains(n));
reqs.dedup();
return Err(Error::missing_required_argument(
&*self.get_required_from(&self.required[..], Some(matcher))
.iter()
.fold(String::new(),
|acc, s| acc + &format!("\n {}", Format::Error(s))[..]),
&*self.create_current_usage(matcher),
self.color())
);
}
}
}
Ok(())
}

fn validate_required(&self, matcher: &ArgMatcher) -> ClapResult<()> {
debugln!("fn=validate_required;required={:?};", self.required);
let c = Colorizer {
Expand Down Expand Up @@ -1948,7 +1980,7 @@ impl<'a, 'b> Parser<'a, 'b>

fn add_defaults(&mut self, matcher: &mut ArgMatcher<'a>) -> ClapResult<()> {
macro_rules! add_val {
($_self:ident, $a:ident, $m:ident) => {
(@default $_self:ident, $a:ident, $m:ident) => {
if let Some(ref val) = $a.v.default_val {
if $m.get($a.b.name).is_none() {
try!($_self.add_val_to_arg($a, OsStr::new(val), $m));
Expand Down Expand Up @@ -1983,9 +2015,10 @@ impl<'a, 'b> Parser<'a, 'b>
}

if done {
continue;
continue; // outer loop (outside macro)
}
}
add_val!(@default $_self, $a, $m)
};
}
for o in &self.opts {
Expand Down
2 changes: 1 addition & 1 deletion src/args/any_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub trait AnyArg<'n, 'e>: std_fmt::Display {
fn id(&self) -> usize;
fn overrides(&self) -> Option<&[&'e str]>;
fn aliases(&self) -> Option<Vec<&'e str>>;
fn requires(&self) -> Option<&[&'e str]>;
fn requires(&self) -> Option<&[(Option<&'e str>, &'n str)]>;
fn blacklist(&self) -> Option<&[&'e str]>;
fn required_unless(&self) -> Option<&[&'e str]>;
fn is_set(&self, ArgSettings) -> bool;
Expand Down
Loading

0 comments on commit 198449d

Please sign in to comment.