Skip to content

Commit

Permalink
feat(Conditional Default Values): adds new Arg::default_value_if[s] m…
Browse files Browse the repository at this point in the history
…ethods for conditional default values

One can now implement conditional default values. I.e. a default value that is only applied in
certain conditions, such as another arg being present, or another arg being present *and*
containing a specific value.

Now it's possible to say, "Only apply this default value if arg X is present" or "Only apply this
value if arg X is present, but also only if arg X's value is equal to Y"

This new method is fully compatible with the current `Arg::default_value`, which gets set only if
the arg wasn't used at runtime *and* none of the specified conditions were met.

Releates to #764
  • Loading branch information
kbknapp committed Dec 29, 2016
1 parent b03eff6 commit eb4010e
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 13 deletions.
3 changes: 2 additions & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::rc::Rc;
use std::result::Result as StdResult;

// Third Party
use vec_map::VecMap;
use vec_map::{self, VecMap};
#[cfg(feature = "yaml")]
use yaml_rust::Yaml;

Expand Down Expand Up @@ -1532,6 +1532,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for App<'n, 'e> {
fn takes_value(&self) -> bool { true }
fn help(&self) -> Option<&'e str> { self.p.meta.about }
fn default_val(&self) -> Option<&'n str> { None }
fn default_vals_ifs(&self) -> Option<vec_map::Values<(&'n str, Option<&'e str>, &'e str)>> {None}
fn longest_filter(&self) -> bool { true }
fn aliases(&self) -> Option<Vec<&'e str>> {
if let Some(ref aliases) = self.p.meta.aliases {
Expand Down
48 changes: 40 additions & 8 deletions src/app/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1949,19 +1949,51 @@ 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) => {
if $m.get($a.b.name).is_none() {
try!($_self.add_val_to_arg($a, OsStr::new($a.v.default_val
.as_ref()
.unwrap()),
$m));
arg_post_processing!($_self, $a, $m);
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));
arg_post_processing!($_self, $a, $m);
}
}
};
}

macro_rules! add_vals_ifs {
($_self:ident, $a:ident, $m:ident) => {
if let Some(ref vm) = $a.v.default_vals_ifs {
let mut done = false;
if $m.get($a.b.name).is_none() {
for &(arg, val, default) in vm.values() {
let add = if let Some(a) = $m.get(arg) {
if let Some(v) = val {
a.vals.values().any(|value| v == value)
} else {
true
}
} else {
false
};
if add {
try!($_self.add_val_to_arg($a, OsStr::new(default), $m));
arg_post_processing!($_self, $a, $m);
done = true;
break;
}
}
}

if done {
continue;
}
}
};
}
for o in self.opts.iter().filter(|o| o.v.default_val.is_some()) {
for o in &self.opts {
add_vals_ifs!(self, o, matcher);
add_val!(self, o, matcher);
}
for p in self.positionals.values().filter(|p| p.v.default_val.is_some()) {
for p in self.positionals.values() {
add_vals_ifs!(self, p, matcher);
add_val!(self, p, matcher);
}
Ok(())
Expand Down
3 changes: 2 additions & 1 deletion src/args/any_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::rc::Rc;
use std::fmt as std_fmt;

// Third Party
use vec_map::VecMap;
use vec_map::{self, VecMap};

// Internal
use args::settings::ArgSettings;
Expand Down Expand Up @@ -33,6 +33,7 @@ pub trait AnyArg<'n, 'e>: std_fmt::Display {
fn val_names(&self) -> Option<&VecMap<&'e str>>;
fn help(&self) -> Option<&'e str>;
fn default_val(&self) -> Option<&'n str>;
fn default_vals_ifs(&self) -> Option<vec_map::Values<(&'n str, Option<&'e str>, &'e str)>>;
fn longest_filter(&self) -> bool;
fn kind(&self) -> ArgKind;
}
Expand Down
208 changes: 208 additions & 0 deletions src/args/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub struct Arg<'a, 'b>
#[doc(hidden)]
pub default_val: Option<&'a str>,
#[doc(hidden)]
pub default_vals_ifs: Option<VecMap<(&'a str, Option<&'b str>, &'b str)>>,
#[doc(hidden)]
pub disp_ord: usize,
#[doc(hidden)]
pub r_unless: Option<Vec<&'a str>>,
Expand Down Expand Up @@ -101,6 +103,7 @@ impl<'a, 'b> Default for Arg<'a, 'b> {
settings: ArgFlags::new(),
val_delim: None,
default_val: None,
default_vals_ifs: None,
disp_ord: 999,
r_unless: None,
}
Expand Down Expand Up @@ -2359,6 +2362,14 @@ impl<'a, 'b> Arg<'a, 'b> {
/// not, consider [`ArgMatches::occurrences_of`] which will return `0` if the argument was *not*
/// used at runtmie.
///
/// **NOTE:** This setting is perfectly compatible with [`Arg::default_value_if`] but slightly
/// different. `Arg::default_value` *only* takes affect when the user has not provided this arg
/// at runtime. `Arg::default_value_if` however only takes affect when the user has not provided
/// a value at runtime **and** these other conditions are met as well. If you have set
/// `Arg::default_value` and `Arg::default_value_if`, and the user **did not** provide a this
/// arg at runtime, nor did were the conditions met for `Arg::default_value_if`, the
/// `Arg::default_value` will be applied.
///
/// **NOTE:** This implicitly sets [`Arg::takes_value(true)`].
///
/// # Examples
Expand Down Expand Up @@ -2400,12 +2411,207 @@ impl<'a, 'b> Arg<'a, 'b> {
/// [`ArgMatches::value_of`]: ./struct.ArgMatches.html#method.value_of
/// [`Arg::takes_value(true)`]: ./struct.Arg.html#method.takes_value
/// [`ArgMatches::is_present`]: ./struct.ArgMatches.html#method.is_present
/// [`Arg::default_value_if`]: ./struct.Arg.html#method.default_value_if
pub fn default_value(mut self, val: &'a str) -> Self {
self.setb(ArgSettings::TakesValue);
self.default_val = Some(val);
self
}

/// Specifies the value of the argument if `arg` has been used at runtime. If `val` is set to
/// `None`, `arg` only needs to be present. If `val` is set to `"some-val"` then `arg` must be
/// present at runtime **and** have the value `val`.
///
/// **NOTE:** This setting is perfectly compatible with [`Arg::default_value`] but slightly
/// different. `Arg::default_value` *only* takes affect when the user has not provided this arg
/// at runtime. This setting however only takes affect when the user has not provided a value at
/// runtime **and** these other conditions are met as well. If you have set `Arg::default_value`
/// and `Arg::default_value_if`, and the user **did not** provide a this arg at runtime, nor did
/// were the conditions met for `Arg::default_value_if`, the `Arg::default_value` will be
/// applied.
///
/// **NOTE:** This implicitly sets [`Arg::takes_value(true)`].
///
/// # Examples
///
/// First we use the default value only if another arg is present at runtime.
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("flag")
/// .long("flag"))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_if("flag", None, "default"))
/// .get_matches_from(vec![
/// "dvif", "--flag"
/// ]);
///
/// assert_eq!(m.value_of("other"), Some("default"));
/// ```
///
/// Next we run the same test, but without providing `--flag`.
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("flag")
/// .long("flag"))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_if("flag", None, "default"))
/// .get_matches_from(vec![
/// "dvif"
/// ]);
///
/// assert_eq!(m.value_of("other"), None);
/// ```
///
/// Now lets only use the default value if `--opt` contains the value `special`.
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("opt")
/// .takes_value(true)
/// .long("opt"))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_if("opt", Some("special"), "default"))
/// .get_matches_from(vec![
/// "dvif", "--opt", "special"
/// ]);
///
/// assert_eq!(m.value_of("other"), Some("default"));
/// ```
///
/// We can run the same test and provide any value *other than* `special` and we won't get a
/// default value.
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("opt")
/// .takes_value(true)
/// .long("opt"))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_if("opt", Some("special"), "default"))
/// .get_matches_from(vec![
/// "dvif", "--opt", "hahaha"
/// ]);
///
/// assert_eq!(m.value_of("other"), None);
/// ```
/// [`Arg::takes_value(true)`]: ./struct.Arg.html#method.takes_value
/// [`Arg::default_value`]: ./struct.Arg.html#method.default_value
pub fn default_value_if(mut self, arg: &'a str, val: Option<&'b str>, default: &'b str) -> Self {
self.setb(ArgSettings::TakesValue);
if let Some(ref mut vm) = self.default_vals_ifs {
let l = vm.len();
vm.insert(l, (arg, val, default));
} else {
let mut vm = VecMap::new();
vm.insert(0, (arg, val, default));
self.default_vals_ifs = Some(vm);
}
self
}

/// Specifies multiple values and conditions in the same manner as [`Arg::default_value_if`].
/// The method takes a slice of tuples in the `(arg, Option<val>, default)` format.
///
/// **NOTE**: The conditions are stored in order and evaluated in the same order. I.e. the first
/// if multiple conditions are true, the first one found will be applied and the ultimate value.
///
/// # Examples
///
/// First we use the default value only if another arg is present at runtime.
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("flag")
/// .long("flag"))
/// .arg(Arg::with_name("opt")
/// .long("opt")
/// .takes_value(true))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_ifs(&[
/// ("flag", None, "default"),
/// ("opt", Some("channal"), "chan"),
/// ])
/// .get_matches_from(vec![
/// "dvif", "--opt", "channal"
/// ]);
///
/// assert_eq!(m.value_of("other"), Some("chan"));
/// ```
///
/// Next we run the same test, but without providing `--flag`.
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("flag")
/// .long("flag"))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_ifs(&[
/// ("flag", None, "default"),
/// ("opt", Some("channal"), "chan"),
/// ])
/// .get_matches_from(vec![
/// "dvif"
/// ]);
///
/// assert_eq!(m.value_of("other"), None);
/// ```
///
/// We can also see that these values are applied in order, and if more than one condition is
/// true, only the first evaluatd "wins"
///
/// ```rust
/// # use clap::{App, Arg};
/// let m = App::new("dvif")
/// .arg(Arg::with_name("flag")
/// .long("flag"))
/// .arg(Arg::with_name("other")
/// .long("other")
/// .default_value_ifs(&[
/// ("flag", None, "default"),
/// ("opt", Some("channal"), "chan"),
/// ])
/// .get_matches_from(vec![
/// "dvif", "--opt", "channal", "--flag"
/// ]);
///
/// assert_eq!(m.value_of("other"), Some("default"));
/// ```
/// [`Arg::takes_value(true)`]: ./struct.Arg.html#method.takes_value
/// [`Arg::default_value`]: ./struct.Arg.html#method.default_value
pub fn default_value_ifs(mut self, ifs: &[(&'a str, Option<&'b str>, &'b str)]) -> Self {
self.setb(ArgSettings::TakesValue);
if let Some(ref mut vm) = self.default_vals_ifs {
let mut l = vm.len();
for &(arg, val, default) in ifs {
vm.insert(l, (arg, val, default));
l += 1;
}
} else {
let mut vm = VecMap::new();
let mut l = 0;
for &(arg, val, default) in ifs {
vm.insert(l, (arg, val, default));
l += 1;
}
self.default_vals_ifs = Some(vm);
}
self
}

/// When set to `true` the help string will be displayed on the line after the argument and
/// indented once. This can be helpful for arguments with very long or complex help messages.
/// This can also be helpful for arguments with very long flag names, or many/long value names.
Expand Down Expand Up @@ -2567,6 +2773,7 @@ impl<'a, 'b, 'z> From<&'z Arg<'a, 'b>> for Arg<'a, 'b> {
settings: a.settings,
val_delim: a.val_delim,
default_val: a.default_val,
default_vals_ifs: a.default_vals_ifs.clone(),
disp_ord: a.disp_ord,
r_unless: a.r_unless.clone(),
}
Expand Down Expand Up @@ -2595,6 +2802,7 @@ impl<'a, 'b> Clone for Arg<'a, 'b> {
settings: self.settings,
val_delim: self.val_delim,
default_val: self.default_val,
default_vals_ifs: self.default_vals_ifs.clone(),
disp_ord: self.disp_ord,
r_unless: self.r_unless.clone(),
}
Expand Down
3 changes: 2 additions & 1 deletion src/args/arg_builder/flag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::rc::Rc;
use std::result::Result as StdResult;

// Third Party
use vec_map::VecMap;
use vec_map::{self, VecMap};

// Internal
use Arg;
Expand Down Expand Up @@ -70,6 +70,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for FlagBuilder<'n, 'e> {
fn val_delim(&self) -> Option<char> { None }
fn help(&self) -> Option<&'e str> { self.b.help }
fn default_val(&self) -> Option<&'n str> { None }
fn default_vals_ifs(&self) -> Option<vec_map::Values<(&'n str, Option<&'e str>, &'e str)>> {None}
fn longest_filter(&self) -> bool { self.s.long.is_some() }
fn aliases(&self) -> Option<Vec<&'e str>> {
if let Some(ref aliases) = self.s.aliases {
Expand Down
3 changes: 2 additions & 1 deletion src/args/arg_builder/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::rc::Rc;
use std::result::Result as StdResult;

// Third Party
use vec_map::VecMap;
use vec_map::{self, VecMap};

// Internal
use args::{ArgSettings, ArgKind, AnyArg, Base, Switched, Valued, Arg, DispOrder};
Expand Down Expand Up @@ -114,6 +114,7 @@ impl<'n, 'e> AnyArg<'n, 'e> for OptBuilder<'n, 'e> {
fn takes_value(&self) -> bool { true }
fn help(&self) -> Option<&'e str> { self.b.help }
fn default_val(&self) -> Option<&'n str> { self.v.default_val }
fn default_vals_ifs(&self) -> Option<vec_map::Values<(&'n str, Option<&'e str>, &'e str)>> { self.v.default_vals_ifs.as_ref().map(|vm| vm.values()) }
fn longest_filter(&self) -> bool { true }
fn aliases(&self) -> Option<Vec<&'e str>> {
if let Some(ref aliases) = self.s.aliases {
Expand Down
Loading

0 comments on commit eb4010e

Please sign in to comment.