Skip to content

Commit

Permalink
fix: Always respect positional occurrences
Browse files Browse the repository at this point in the history
When supporting multiple occurrences for positional arguments in clap-rs#2804,
I added some tests to cover this but apparently simpler cases fail
despite those more complicated cases being tested.

This adds more multiple-occurrences tests for positional arguments,
fixes them, and in general equates multiple values with occurrences for
positional arguments as part of clap-rs#2692.  There are a couple more points
for consideration for clap-rs#2692 for us to decide on once this unblocks them
(usage special case in clap-rs#2977 and how subcommand help should be handled).

I fully admit I have not fully quantified the impact of all of these
changes and am heavily relying on the quality of our tests to carry this
forward.
  • Loading branch information
epage committed Nov 1, 2021
1 parent 96e7dfe commit 4f1a25b
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 17 deletions.
4 changes: 3 additions & 1 deletion clap_generate/src/generators/shells/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,9 @@ fn write_positionals_of(p: &App) -> String {
for arg in p.get_positionals() {
debug!("write_positionals_of:iter: arg={}", arg.get_name());

let cardinality = if arg.is_set(ArgSettings::MultipleValues) {
let cardinality = if arg.is_set(ArgSettings::MultipleValues)
|| arg.is_set(ArgSettings::MultipleOccurrences)
{
"*:"
} else if !arg.is_set(ArgSettings::Required) {
":"
Expand Down
7 changes: 6 additions & 1 deletion src/build/arg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4912,6 +4912,11 @@ impl<'help> Arg<'help> {
Cow::Borrowed(self.name)
}
}

/// Either multiple values or occurrences
pub(crate) fn is_multiple(&self) -> bool {
self.is_set(ArgSettings::MultipleValues) | self.is_set(ArgSettings::MultipleOccurrences)
}
}

#[cfg(feature = "yaml")]
Expand Down Expand Up @@ -5109,7 +5114,7 @@ where
write(&delim.to_string(), false)?;
}
}
if num_val_names == 1 && mult_val {
if (num_val_names == 1 && mult_val) || (arg.is_positional() && mult_occ) {
write("...", true)?;
}
}
Expand Down
30 changes: 15 additions & 15 deletions src/parse/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,8 @@ impl<'help, 'app> Parser<'help, 'app> {
num_p
);

// Next we verify that only the highest index has a .multiple_values(true) (if any)
let only_highest = |a: &Arg| {
a.is_set(ArgSettings::MultipleValues) && (a.index.unwrap_or(0) != highest_idx)
};
// Next we verify that only the highest index has takes multiple arguments (if any)
let only_highest = |a: &Arg| a.is_multiple() && (a.index.unwrap_or(0) != highest_idx);
if self.app.get_positionals().any(only_highest) {
// First we make sure if there is a positional that allows multiple values
// the one before it (second to last) has one of these:
Expand All @@ -208,8 +206,7 @@ impl<'help, 'app> Parser<'help, 'app> {
);

// We make sure if the second to last is Multiple the last is ArgSettings::Last
let ok = second_to_last.is_set(ArgSettings::MultipleValues)
|| last.is_set(ArgSettings::Last);
let ok = second_to_last.is_multiple() || last.is_set(ArgSettings::Last);
assert!(
ok,
"Only the last positional argument, or second to last positional \
Expand All @@ -220,12 +217,15 @@ impl<'help, 'app> Parser<'help, 'app> {
let count = self
.app
.get_positionals()
.filter(|p| p.settings.is_set(ArgSettings::MultipleValues) && p.num_vals.is_none())
.filter(|p| {
p.settings.is_set(ArgSettings::MultipleOccurrences)
|| (p.settings.is_set(ArgSettings::MultipleValues) && p.num_vals.is_none())
})
.count();
let ok = count <= 1
|| (last.is_set(ArgSettings::Last)
&& last.is_set(ArgSettings::MultipleValues)
&& second_to_last.is_set(ArgSettings::MultipleValues)
&& last.is_multiple()
&& second_to_last.is_multiple()
&& count == 2);
assert!(
ok,
Expand Down Expand Up @@ -390,12 +390,12 @@ impl<'help, 'app> Parser<'help, 'app> {
let is_second_to_last = pos_counter + 1 == positional_count;

// The last positional argument, or second to last positional
// argument may be set to .multiple_values(true)
// argument may be set to .multiple_values(true) or `.multiple_occurrences(true)`
let low_index_mults = is_second_to_last
&& self.app.get_positionals().any(|a| {
a.is_set(ArgSettings::MultipleValues)
&& (positional_count != a.index.unwrap_or(0))
})
&& self
.app
.get_positionals()
.any(|a| a.is_multiple() && (positional_count != a.index.unwrap_or(0)))
&& self
.app
.get_positionals()
Expand Down Expand Up @@ -683,7 +683,7 @@ impl<'help, 'app> Parser<'help, 'app> {
self.inc_occurrence_of_arg(matcher, p);

// Only increment the positional counter if it doesn't allow multiples
if !p.settings.is_set(ArgSettings::MultipleValues) {
if !p.is_multiple() {
pos_counter += 1;
parse_state = ParseState::ValuesDone;
} else {
Expand Down
100 changes: 100 additions & 0 deletions tests/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,106 @@ OPTIONS:
));
}

#[test]
fn positional_multiple_values_is_dotted() {
let app = App::new("test").arg(
Arg::new("foo")
.required(true)
.takes_value(true)
.multiple_values(true),
);
assert!(utils::compare_output(
app,
"test --help",
"test
USAGE:
test <foo>...
ARGS:
<foo>...
OPTIONS:
-h, --help Print help information
",
false
));

let app = App::new("test").arg(
Arg::new("foo")
.required(true)
.takes_value(true)
.value_name("BAR")
.multiple_values(true),
);
assert!(utils::compare_output(
app,
"test --help",
"test
USAGE:
test <BAR>...
ARGS:
<BAR>...
OPTIONS:
-h, --help Print help information
",
false
));
}

#[test]
fn positional_multiple_occurrences_is_dotted() {
let app = App::new("test").arg(
Arg::new("foo")
.required(true)
.takes_value(true)
.multiple_occurrences(true),
);
assert!(utils::compare_output(
app,
"test --help",
"test
USAGE:
test <foo>...
ARGS:
<foo>...
OPTIONS:
-h, --help Print help information
",
false
));

let app = App::new("test").arg(
Arg::new("foo")
.required(true)
.takes_value(true)
.value_name("BAR")
.multiple_occurrences(true),
);
assert!(utils::compare_output(
app,
"test --help",
"test
USAGE:
test <BAR>...
ARGS:
<BAR>...
OPTIONS:
-h, --help Print help information
",
false
));
}

#[test]
fn disabled_help_flag() {
let res = App::new("foo")
Expand Down
56 changes: 56 additions & 0 deletions tests/multiple_occurrences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,38 @@ fn multiple_occurrences_of_flags_mixed() {
assert_eq!(m.occurrences_of("flag"), 1);
}

#[test]
fn multiple_occurrences_of_positional() {
let app = App::new("test").arg(Arg::new("multi").setting(ArgSettings::MultipleOccurrences));

let m = app
.clone()
.try_get_matches_from(&["test"])
.expect("zero occurrences work");
assert!(!m.is_present("multi"));
assert_eq!(m.occurrences_of("multi"), 0);
assert!(m.values_of("multi").is_none());

let m = app
.clone()
.try_get_matches_from(&["test", "one"])
.expect("single occurrence work");
assert!(m.is_present("multi"));
assert_eq!(m.occurrences_of("multi"), 1);
assert_eq!(m.values_of("multi").unwrap().collect::<Vec<_>>(), ["one"]);

let m = app
.clone()
.try_get_matches_from(&["test", "one", "two", "three", "four"])
.expect("many occurrences work");
assert!(m.is_present("multi"));
assert_eq!(m.occurrences_of("multi"), 4);
assert_eq!(
m.values_of("multi").unwrap().collect::<Vec<_>>(),
["one", "two", "three", "four"]
);
}

#[test]
fn multiple_occurrences_of_flags_large_quantity() {
let args: Vec<&str> = vec![""]
Expand Down Expand Up @@ -194,3 +226,27 @@ fn max_occurrences_try_inputs() {
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::TooManyOccurrences);
}

#[test]
fn max_occurrences_positional() {
let app = App::new("prog").arg(Arg::new("verbose").max_occurrences(3));
let m = app.clone().try_get_matches_from(vec!["prog", "v"]);
assert!(m.is_ok(), "{}", m.unwrap_err());
assert_eq!(m.unwrap().occurrences_of("verbose"), 1);

let m = app.clone().try_get_matches_from(vec!["prog", "v", "v"]);
assert!(m.is_ok(), "{}", m.unwrap_err());
assert_eq!(m.unwrap().occurrences_of("verbose"), 2);

let m = app
.clone()
.try_get_matches_from(vec!["prog", "v", "v", "v"]);
assert!(m.is_ok(), "{}", m.unwrap_err());
assert_eq!(m.unwrap().occurrences_of("verbose"), 3);

let m = app
.clone()
.try_get_matches_from(vec!["prog", "v", "v", "v", "v"]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::TooManyOccurrences);
}

0 comments on commit 4f1a25b

Please sign in to comment.