Skip to content

Commit

Permalink
Add number control of grouped value number.
Browse files Browse the repository at this point in the history
  • Loading branch information
ldm0 committed Jan 28, 2021
1 parent 4d8d64a commit 912602a
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 15 deletions.
24 changes: 24 additions & 0 deletions src/build/arg/mod.rs
Expand Up @@ -89,6 +89,9 @@ pub struct Arg<'help> {
pub(crate) num_vals: Option<u64>,
pub(crate) max_vals: Option<u64>,
pub(crate) min_vals: Option<u64>,
pub(crate) grouped_num_vals: Option<u64>,
pub(crate) grouped_max_vals: Option<u64>,
pub(crate) grouped_min_vals: Option<u64>,
pub(crate) validator: Option<Arc<Mutex<Validator<'help>>>>,
pub(crate) validator_os: Option<Arc<Mutex<ValidatorOs<'help>>>>,
pub(crate) val_delim: Option<char>,
Expand Down Expand Up @@ -1851,6 +1854,13 @@ impl<'help> Arg<'help> {
self.takes_value(true)
}

/// Placeholder documentation
#[inline]
pub fn grouped_number_of_values(mut self, qty: u64) -> Self {
self.grouped_num_vals = Some(qty);
self.takes_value(true)
}

/// Allows one to perform a custom validation on the argument value. You provide a closure
/// which accepts a [`String`] value, and return a [`Result`] where the [`Err(String)`] is a
/// message displayed to the user.
Expand Down Expand Up @@ -2073,6 +2083,13 @@ impl<'help> Arg<'help> {
self.takes_value(true).multiple_values(true)
}

/// placeholder documentation
#[inline]
pub fn grouped_max_values(mut self, qty: u64) -> Self {
self.grouped_max_vals = Some(qty);
self.takes_value(true).multiple_values(true)
}

/// Specifies the *minimum* number of values for this argument. For example, if you had a
/// `-f <file>` argument where you wanted at least 2 'files' you would set
/// `.min_values(2)`, and this argument would be satisfied if the user provided, 2 or more
Expand Down Expand Up @@ -2136,6 +2153,13 @@ impl<'help> Arg<'help> {
self.takes_value(true)
}

/// placeholder documentation
#[inline]
pub fn grouped_min_values(mut self, qty: u64) -> Self {
self.grouped_min_vals = Some(qty);
self.takes_value(true)
}

/// Specifies the separator to use when values are clumped together, defaults to `,` (comma).
///
/// **NOTE:** implicitly sets [`Arg::use_delimiter(true)`]
Expand Down
8 changes: 8 additions & 0 deletions src/parse/matches/matched_arg.rs
Expand Up @@ -85,6 +85,14 @@ impl MatchedArg {
self.vals.last().map(|x| x.len()).unwrap_or(0)
}

pub(crate) fn min_num_vals_group(&self) -> usize {
self.vals.iter().map(|x| x.len()).min().unwrap_or(0)
}

pub(crate) fn max_num_vals_group(&self) -> usize {
self.vals.iter().map(|x| x.len()).max().unwrap_or(0)
}

pub(crate) fn no_val(&self) -> bool {
self.vals.iter().flatten().count() == 0
}
Expand Down
86 changes: 74 additions & 12 deletions src/parse/validator.rs
Expand Up @@ -442,30 +442,74 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
fn validate_arg_num_vals(&self, a: &Arg, ma: &MatchedArg) -> ClapResult<()> {
debug!("Validator::validate_arg_num_vals");
if let Some(num) = a.num_vals {
let num_vals = ma.num_vals() as u64;
debug!("Validator::validate_arg_num_vals: num_vals set...{}", num);
let should_err = if a.is_set(ArgSettings::MultipleValues) {
((ma.num_vals() as u64) % num) != 0
let incorrect_num = if num_vals != num {
Some(num_vals)
} else {
num != (ma.num_vals() as u64)
None
};
if should_err {
if let Some(incorrect_num) = incorrect_num {
debug!("Validator::validate_arg_num_vals: Sending error WrongNumberOfValues");
return Err(Error::wrong_number_of_values(
a,
num,
if a.is_set(ArgSettings::MultipleValues) {
ma.num_vals() % num as usize
} else {
ma.num_vals()
},
incorrect_num as usize,
Usage::new(self.p).create_usage_with_title(&[]),
self.p.app.color(),
));
}
}
if let Some(num) = a.grouped_num_vals {
let max_num_vals = ma.max_num_vals_group() as u64;
let min_num_vals = ma.min_num_vals_group() as u64;
debug!(
"Validator::validate_arg_num_vals: grouped_num_vals set...{}",
num
);
let incorrect_num = if max_num_vals != num {
Some(max_num_vals)
} else if min_num_vals != num {
Some(min_num_vals)
} else {
None
};
if let Some(incorrect_num) = incorrect_num {
debug!("Validator::validate_arg_num_vals: Sending error WrongNumberOfValues");
return Err(Error::wrong_number_of_values(
a,
num,
incorrect_num as usize,
Usage::new(self.p).create_usage_with_title(&[]),
self.p.app.color(),
));
}
}
if let Some(num) = a.max_vals {
let num_vals = ma.num_vals() as u64;
debug!("Validator::validate_arg_num_vals: max_vals set...{}", num);
if (ma.num_vals() as u64) > num {
if num_vals > num {
debug!("Validator::validate_arg_num_vals: Sending error TooManyValues");
return Err(Error::too_many_values(
ma.vals_flatten()
.last()
.expect(INTERNAL_ERROR_MSG)
.to_str()
.expect(INVALID_UTF8)
.to_string(),
a,
Usage::new(self.p).create_usage_with_title(&[]),
self.p.app.color(),
));
}
}
if let Some(num) = a.grouped_max_vals {
let max_num_vals = ma.max_num_vals_group() as u64;
debug!(
"Validator::validate_arg_num_vals: grouped_max_vals set...{}",
num
);
if max_num_vals > num {
debug!("Validator::validate_arg_num_vals: Sending error TooManyValues");
return Err(Error::too_many_values(
ma.vals_flatten()
Expand All @@ -481,13 +525,14 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
}
}
let min_vals_zero = if let Some(num) = a.min_vals {
let num_vals = ma.num_vals() as u64;
debug!("Validator::validate_arg_num_vals: min_vals set: {}", num);
if (ma.num_vals() as u64) < num && num != 0 {
if num_vals < num && num != 0 {
debug!("Validator::validate_arg_num_vals: Sending error TooFewValues");
return Err(Error::too_few_values(
a,
num,
ma.num_vals(),
num_vals as usize,
Usage::new(self.p).create_usage_with_title(&[]),
self.p.app.color(),
));
Expand All @@ -496,6 +541,23 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
} else {
false
};
if let Some(num) = a.grouped_min_vals {
let min_num_vals = ma.min_num_vals_group() as u64;
debug!(
"Validator::validate_arg_num_vals: grouped_min_vals set: {}",
num
);
if min_num_vals < num {
debug!("Validator::validate_arg_num_vals: Sending error TooFewValues");
return Err(Error::too_few_values(
a,
num,
min_num_vals as usize,
Usage::new(self.p).create_usage_with_title(&[]),
self.p.app.color(),
));
}
}
// Issue 665 (https://github.com/kbknapp/clap-rs/issues/665)
// Issue 1105 (https://github.com/kbknapp/clap-rs/issues/1105)
if a.is_set(ArgSettings::TakesValue) && !min_vals_zero && ma.no_val() {
Expand Down
2 changes: 1 addition & 1 deletion tests/app_settings.rs
Expand Up @@ -944,7 +944,7 @@ fn aaos_opts_mult() {
.setting(AppSettings::AllArgsOverrideSelf)
.arg(
Arg::from("--opt [val]... 'some option'")
.number_of_values(1)
.grouped_min_values(1)
.require_delimiter(true),
)
.try_get_matches_from(vec!["", "--opt=some", "--opt=other", "--opt=one,two"]);
Expand Down
90 changes: 90 additions & 0 deletions tests/grouped_values.rs
Expand Up @@ -188,3 +188,93 @@ fn issue_2171() {
let _ = schema.clone().try_get_matches_from(argv).unwrap();
}
}

#[test]
fn grouped_min_values_test() {
let m = App::new("myapp")
.arg(
Arg::new("option")
.short('o')
.takes_value(true)
.use_delimiter(true)
.multiple(true)
.grouped_min_values(1),
)
.get_matches_from(vec!["myapp", "-o=foo", "-o=val1,val2,val3", "-o=bar"]);
let grouped_vals: Vec<_> = m.grouped_values_of("option").unwrap().collect();
assert_eq!(
grouped_vals,
vec![vec!["foo"], vec!["val1", "val2", "val3"], vec!["bar"]]
);
}

#[test]
fn grouped_max_values_test() {
let m = App::new("myapp")
.arg(
Arg::new("option")
.short('o')
.takes_value(true)
.use_delimiter(true)
.multiple(true)
.grouped_max_values(3),
)
.get_matches_from(vec!["myapp", "-o=foo", "-o=val1,val2,val3", "-o=bar"]);
let grouped_vals: Vec<_> = m.grouped_values_of("option").unwrap().collect();
assert_eq!(
grouped_vals,
vec![vec!["foo"], vec!["val1", "val2", "val3"], vec!["bar"]]
);
}

#[test]
fn grouped_min_values_failed() {
static MIN_VALS_FAILED: &str =
"error: The argument '-o <option>...' requires at least 2 values, but only 1 was provided
USAGE:
myapp [OPTIONS]
For more information try --help";
let app = App::new("myapp").arg(
Arg::new("option")
.short('o')
.takes_value(true)
.use_delimiter(true)
.multiple(true)
.grouped_min_values(2),
);

utils::compare_output(
app,
"myapp -o=foo -o=val1,val2,val3 -o=bar",
MIN_VALS_FAILED,
true,
);
}

#[test]
fn grouped_max_values_failed() {
static MAX_VALS_FAILED: &str =
"error: The value 'bar' was provided to '-o <option>...' but it wasn't expecting any more values
USAGE:
myapp [OPTIONS]
For more information try --help";
let app = App::new("myapp").arg(
Arg::new("option")
.short('o')
.takes_value(true)
.use_delimiter(true)
.multiple(true)
.grouped_max_values(2),
);

utils::compare_output(
app,
"myapp -o=foo -o=val1,val2,val3 -o=bar",
MAX_VALS_FAILED,
true,
);
}
2 changes: 1 addition & 1 deletion tests/multiple_values.rs
Expand Up @@ -130,7 +130,7 @@ fn option_exact_exact_mult() {
.about("multiple options")
.takes_value(true)
.multiple(true)
.number_of_values(3),
.grouped_number_of_values(3),
)
.try_get_matches_from(vec![
"", "-o", "val1", "val2", "val3", "-o", "val4", "val5", "val6",
Expand Down
2 changes: 1 addition & 1 deletion tests/posix_compatible.rs
Expand Up @@ -40,7 +40,7 @@ fn mult_option_require_delim_overrides_itself() {
.arg(
Arg::from("--opt [val]... 'some option'")
.overrides_with("opt")
.number_of_values(1)
.grouped_min_values(1)
.require_delimiter(true),
)
.try_get_matches_from(vec!["", "--opt=some", "--opt=other", "--opt=one,two"]);
Expand Down

0 comments on commit 912602a

Please sign in to comment.