Skip to content

Commit 1ced2a7

Browse files
committed
feat(Positional Args): allows specifying the second to last positional argument as multiple(true)
Now one can build CLIs that support things like `mv <files>... <target>` There are a few requirements and caveats; * The final positional argument (and all positional arguments prior) *must* be required * Only one positional argument may be `multiple(true)` * Only the second to last, or last positional argument may be `multiple(true)` Closes #725
1 parent 1d6f8fd commit 1ced2a7

File tree

4 files changed

+143
-64
lines changed

4 files changed

+143
-64
lines changed

src/app/macros.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ macro_rules! validate_multiples {
131131
($_self:ident, $a:ident, $m:ident) => {
132132
debugln!("macro=validate_multiples!;");
133133
if $m.contains(&$a.name) && !$a.settings.is_set(ArgSettings::Multiple) {
134-
// Not the first time, and we don't allow multiples
134+
// Not the first time, and we don't allow multiples
135135
return Err(Error::unexpected_multiple_usage($a,
136136
&*$_self.create_current_usage($m),
137137
$_self.color()))

src/app/mod.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1250,7 +1250,7 @@ impl<'a, 'b> App<'a, 'b> {
12501250
/// [`AppSettings::NoBinaryName`]: ./enum.AppSettings.html#variant.NoBinaryName
12511251
pub fn get_matches_from<I, T>(mut self, itr: I) -> ArgMatches<'a>
12521252
where I: IntoIterator<Item = T>,
1253-
T: Into<OsString>
1253+
T: Into<OsString> + Clone
12541254
{
12551255
self.get_matches_from_safe_borrow(itr).unwrap_or_else(|e| {
12561256
// Otherwise, write to stderr and exit
@@ -1292,7 +1292,7 @@ impl<'a, 'b> App<'a, 'b> {
12921292
/// [`AppSettings::NoBinaryName`]: ./enum.AppSettings.html#variant.NoBinaryName
12931293
pub fn get_matches_from_safe<I, T>(mut self, itr: I) -> ClapResult<ArgMatches<'a>>
12941294
where I: IntoIterator<Item = T>,
1295-
T: Into<OsString>
1295+
T: Into<OsString> + Clone
12961296
{
12971297
self.get_matches_from_safe_borrow(itr)
12981298
}
@@ -1320,7 +1320,7 @@ impl<'a, 'b> App<'a, 'b> {
13201320
/// [`AppSettings::NoBinaryName`]: ./enum.AppSettings.html#variant.NoBinaryName
13211321
pub fn get_matches_from_safe_borrow<I, T>(&mut self, itr: I) -> ClapResult<ArgMatches<'a>>
13221322
where I: IntoIterator<Item = T>,
1323-
T: Into<OsString>
1323+
T: Into<OsString> + Clone
13241324
{
13251325
// Verify all positional assertions pass
13261326
self.p.verify_positionals();
@@ -1355,7 +1355,7 @@ impl<'a, 'b> App<'a, 'b> {
13551355
}
13561356

13571357
// do the real parsing
1358-
if let Err(e) = self.p.get_matches_with(&mut matcher, &mut it) {
1358+
if let Err(e) = self.p.get_matches_with(&mut matcher, &mut it.peekable()) {
13591359
return Err(e);
13601360
}
13611361

src/app/parser.rs

+102-31
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::io::{self, BufWriter, Write};
88
use std::os::unix::ffi::OsStrExt;
99
use std::path::PathBuf;
1010
use std::slice::Iter;
11+
use std::iter::Peekable;
1112

1213
// Third Party
1314
use vec_map::{self, VecMap};
@@ -118,15 +119,14 @@ impl<'a, 'b> Parser<'a, 'b>
118119
let out_dir = PathBuf::from(od);
119120
let name = &*self.meta.bin_name.as_ref().unwrap().clone();
120121
let file_name = match for_shell {
121-
122+
122123
Shell::Bash => format!("{}.bash-completion", name),
123124
Shell::Fish => format!("{}.fish", name),
124-
Shell::Zsh => format!("_{}", name)
125+
Shell::Zsh => format!("_{}", name),
125126
};
126127

127128
let mut file = match File::create(out_dir.join(file_name)) {
128-
Err(why) => panic!("couldn't create completion file: {}",
129-
why.description()),
129+
Err(why) => panic!("couldn't create completion file: {}", why.description()),
130130
Ok(file) => file,
131131
};
132132
self.gen_completions_to(for_shell, &mut file)
@@ -266,10 +266,16 @@ impl<'a, 'b> Parser<'a, 'b>
266266
for (i, o) in self.opts.iter_mut().enumerate().filter(|&(_, ref o)| o.disp_ord == 999) {
267267
o.disp_ord = if unified { o.unified_ord } else { i };
268268
}
269-
for (i, f) in self.flags.iter_mut().enumerate().filter(|&(_, ref f)| f.disp_ord == 999) {
269+
for (i, f) in self.flags
270+
.iter_mut()
271+
.enumerate()
272+
.filter(|&(_, ref f)| f.disp_ord == 999) {
270273
f.disp_ord = if unified { f.unified_ord } else { i };
271274
}
272-
for (i, sc) in &mut self.subcommands.iter_mut().enumerate().filter(|&(_, ref sc)| sc.p.meta.disp_ord == 999) {
275+
for (i, sc) in &mut self.subcommands
276+
.iter_mut()
277+
.enumerate()
278+
.filter(|&(_, ref sc)| sc.p.meta.disp_ord == 999) {
273279
sc.p.meta.disp_ord = i;
274280
}
275281
}
@@ -517,12 +523,40 @@ impl<'a, 'b> Parser<'a, 'b>
517523
}
518524

519525
// Next we verify that only the highest index has a .multiple(true) (if any)
520-
debug_assert!(!self.positionals
521-
.values()
522-
.any(|a| a.settings.is_set(ArgSettings::Multiple) &&
523-
(a.index as usize != self.positionals.len())
524-
),
525-
"Only the positional argument with the highest index may accept multiple values");
526+
if self.positionals()
527+
.any(|a| {
528+
a.settings.is_set(ArgSettings::Multiple) &&
529+
(a.index as usize != self.positionals.len())
530+
}) {
531+
debug_assert!(self.positionals()
532+
.filter(|p| p.settings.is_set(ArgSettings::Multiple)
533+
&& p.num_vals.is_none()).map(|_| 1).sum::<u64>() <= 1,
534+
"Only one positional argument with .multiple(true) set is allowed per command");
535+
536+
debug_assert!(self.positionals()
537+
.rev()
538+
.next()
539+
.unwrap()
540+
.is_set(ArgSettings::Required),
541+
"When using a positional argument with .multiple(true) that is *not the last* \
542+
positional argument, the last positional argument (i.e the one with the highest \
543+
index) *must* have .required(true) set.");
544+
545+
debug_assert!({
546+
let num = self.positionals.len() - 1;
547+
self.positionals.get(num).unwrap().is_set(ArgSettings::Multiple)
548+
},
549+
"Only the last positional argument, or second to last positional argument may be set to .multiple(true)");
550+
551+
self.set(AppSettings::LowIndexMultiplePositional);
552+
}
553+
554+
debug_assert!(self.positionals()
555+
.filter(|p| p.settings.is_set(ArgSettings::Multiple)
556+
&& p.num_vals.is_none())
557+
.map(|_| 1)
558+
.sum::<u64>() <= 1,
559+
"Only one positional argument with .multiple(true) set is allowed per command");
526560

527561
// If it's required we also need to ensure all previous positionals are
528562
// required too
@@ -666,10 +700,10 @@ impl<'a, 'b> Parser<'a, 'b>
666700
#[cfg_attr(feature = "lints", allow(while_let_on_iterator))]
667701
pub fn get_matches_with<I, T>(&mut self,
668702
matcher: &mut ArgMatcher<'a>,
669-
it: &mut I)
703+
it: &mut Peekable<I>)
670704
-> ClapResult<()>
671705
where I: Iterator<Item = T>,
672-
T: Into<OsString>
706+
T: Into<OsString> + Clone
673707
{
674708
debugln!("fn=get_matches_with;");
675709
// First we create the `--help` and `--version` arguments and add them if
@@ -684,15 +718,7 @@ impl<'a, 'b> Parser<'a, 'b>
684718
debugln!("Begin parsing '{:?}' ({:?})", arg_os, &*arg_os.as_bytes());
685719

686720
// Is this a new argument, or values from a previous option?
687-
debug!("Starts new arg...");
688-
let starts_new_arg = if arg_os.starts_with(b"-") {
689-
sdebugln!("Maybe");
690-
// a singe '-' by itself is a value and typically means "stdin" on unix systems
691-
!(arg_os.len_() == 1)
692-
} else {
693-
sdebugln!("No");
694-
false
695-
};
721+
let starts_new_arg = is_new_arg(&arg_os);
696722

697723
// Has the user already passed '--'? Meaning only positional args follow
698724
if !self.trailing_vals {
@@ -739,15 +765,16 @@ impl<'a, 'b> Parser<'a, 'b>
739765
arg_os.to_string_lossy().parse::<f64>().is_ok()));
740766
if needs_val_of.is_none() {
741767
if self.is_set(AppSettings::AllowNegativeNumbers) {
742-
if !(arg_os.to_string_lossy().parse::<i64>().is_ok() || arg_os.to_string_lossy().parse::<f64>().is_ok()) {
768+
if !(arg_os.to_string_lossy().parse::<i64>().is_ok() ||
769+
arg_os.to_string_lossy().parse::<f64>().is_ok()) {
743770
return Err(Error::unknown_argument(&*arg_os.to_string_lossy(),
744771
"",
745772
&*self.create_current_usage(matcher),
746773
self.color()));
747774
}
748775
} else if !self.is_set(AppSettings::AllowLeadingHyphen) {
749776
continue;
750-
}
777+
}
751778
} else {
752779
continue;
753780
}
@@ -775,6 +802,26 @@ impl<'a, 'b> Parser<'a, 'b>
775802
}
776803
}
777804

805+
debugln!("Positional counter...{}", pos_counter);
806+
debug!("Checking for low index multiples...");
807+
if self.is_set(AppSettings::LowIndexMultiplePositional) && pos_counter == (self.positionals.len() - 1) {
808+
sdebugln!("Found");
809+
if let Some(na) = it.peek() {
810+
let n = (*na).clone().into();
811+
if is_new_arg(&n) || self.possible_subcommand(&n) || suggestions::did_you_mean(&n.to_string_lossy(),
812+
self.subcommands
813+
.iter()
814+
.map(|s| &s.p.meta.name)).is_some() {
815+
debugln!("Bumping the positional counter...");
816+
pos_counter += 1;
817+
}
818+
} else {
819+
debugln!("Bumping the positional counter...");
820+
pos_counter += 1;
821+
}
822+
} else {
823+
sdebugln!("None");
824+
}
778825
if let Some(p) = self.positionals.get(pos_counter) {
779826
parse_positional!(self, p, arg_os, pos_counter, matcher);
780827
} else if self.settings.is_set(AppSettings::AllowExternalSubcommands) {
@@ -953,10 +1000,10 @@ impl<'a, 'b> Parser<'a, 'b>
9531000
fn parse_subcommand<I, T>(&mut self,
9541001
sc_name: String,
9551002
matcher: &mut ArgMatcher<'a>,
956-
it: &mut I)
1003+
it: &mut Peekable<I>)
9571004
-> ClapResult<()>
9581005
where I: Iterator<Item = T>,
959-
T: Into<OsString>
1006+
T: Into<OsString> + Clone
9601007
{
9611008
use std::fmt::Write;
9621009
debugln!("fn=parse_subcommand;");
@@ -1949,10 +1996,11 @@ impl<'a, 'b> Parser<'a, 'b>
19491996
}
19501997
None
19511998
}
1952-
1999+
19532000
fn find_flag(&self, name: &str) -> Option<&FlagBuilder<'a, 'b>> {
19542001
for f in self.flags() {
1955-
if f.name == name || f.aliases.as_ref().unwrap_or(&vec![("",false)]).iter().any(|&(n,_)| n == name) {
2002+
if f.name == name ||
2003+
f.aliases.as_ref().unwrap_or(&vec![("",false)]).iter().any(|&(n, _)| n == name) {
19562004
return Some(f);
19572005
}
19582006
}
@@ -1961,7 +2009,8 @@ impl<'a, 'b> Parser<'a, 'b>
19612009

19622010
fn find_option(&self, name: &str) -> Option<&OptBuilder<'a, 'b>> {
19632011
for o in self.opts() {
1964-
if o.name == name || o.aliases.as_ref().unwrap_or(&vec![("",false)]).iter().any(|&(n,_)| n == name) {
2012+
if o.name == name ||
2013+
o.aliases.as_ref().unwrap_or(&vec![("",false)]).iter().any(|&(n, _)| n == name) {
19652014
return Some(o);
19662015
}
19672016
}
@@ -1983,7 +2032,15 @@ impl<'a, 'b> Parser<'a, 'b>
19832032
debugln!("Looking for sc...{}", sc);
19842033
debugln!("Currently in Parser...{}", self.meta.bin_name.as_ref().unwrap());
19852034
for s in self.subcommands.iter() {
1986-
if s.p.meta.bin_name.as_ref().unwrap_or(&String::new()) == sc || (s.p.meta.aliases.is_some() && s.p.meta.aliases.as_ref().unwrap().iter().any(|&(s,_)| s == sc.split(' ').rev().next().expect(INTERNAL_ERROR_MSG))) {
2035+
if s.p.meta.bin_name.as_ref().unwrap_or(&String::new()) == sc ||
2036+
(s.p.meta.aliases.is_some() &&
2037+
s.p
2038+
.meta
2039+
.aliases
2040+
.as_ref()
2041+
.unwrap()
2042+
.iter()
2043+
.any(|&(s, _)| s == sc.split(' ').rev().next().expect(INTERNAL_ERROR_MSG))) {
19872044
return Some(s);
19882045
}
19892046
if let Some(app) = s.p.find_subcommand(sc) {
@@ -2019,3 +2076,17 @@ impl<'a, 'b> Clone for Parser<'a, 'b>
20192076
}
20202077
}
20212078
}
2079+
2080+
#[inline]
2081+
fn is_new_arg(arg_os: &OsStr) -> bool {
2082+
// Is this a new argument, or values from a previous option?
2083+
debug!("Starts new arg...");
2084+
if arg_os.starts_with(b"-") {
2085+
sdebugln!("Maybe");
2086+
// a singe '-' by itself is a value and typically means "stdin" on unix systems
2087+
!(arg_os.len_() == 1)
2088+
} else {
2089+
sdebugln!("No");
2090+
false
2091+
}
2092+
}

src/app/settings.rs

+36-28
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,35 @@ use std::str::FromStr;
44

55
bitflags! {
66
flags Flags: u32 {
7-
const SC_NEGATE_REQS = 0b0000000000000000000000000001,
8-
const SC_REQUIRED = 0b0000000000000000000000000010,
9-
const A_REQUIRED_ELSE_HELP = 0b0000000000000000000000000100,
10-
const GLOBAL_VERSION = 0b0000000000000000000000001000,
11-
const VERSIONLESS_SC = 0b0000000000000000000000010000,
12-
const UNIFIED_HELP = 0b0000000000000000000000100000,
13-
const WAIT_ON_ERROR = 0b0000000000000000000001000000,
14-
const SC_REQUIRED_ELSE_HELP= 0b0000000000000000000010000000,
15-
const NEEDS_LONG_HELP = 0b0000000000000000000100000000,
16-
const NEEDS_LONG_VERSION = 0b0000000000000000001000000000,
17-
const NEEDS_SC_HELP = 0b0000000000000000010000000000,
18-
const DISABLE_VERSION = 0b0000000000000000100000000000,
19-
const HIDDEN = 0b0000000000000001000000000000,
20-
const TRAILING_VARARG = 0b0000000000000010000000000000,
21-
const NO_BIN_NAME = 0b0000000000000100000000000000,
22-
const ALLOW_UNK_SC = 0b0000000000001000000000000000,
23-
const UTF8_STRICT = 0b0000000000010000000000000000,
24-
const UTF8_NONE = 0b0000000000100000000000000000,
25-
const LEADING_HYPHEN = 0b0000000001000000000000000000,
26-
const NO_POS_VALUES = 0b0000000010000000000000000000,
27-
const NEXT_LINE_HELP = 0b0000000100000000000000000000,
28-
const DERIVE_DISP_ORDER = 0b0000001000000000000000000000,
29-
const COLORED_HELP = 0b0000010000000000000000000000,
30-
const COLOR_ALWAYS = 0b0000100000000000000000000000,
31-
const COLOR_AUTO = 0b0001000000000000000000000000,
32-
const COLOR_NEVER = 0b0010000000000000000000000000,
33-
const DONT_DELIM_TRAIL = 0b0100000000000000000000000000,
34-
const ALLOW_NEG_NUMS = 0b1000000000000000000000000000,
7+
const SC_NEGATE_REQS = 0b00000000000000000000000000001,
8+
const SC_REQUIRED = 0b00000000000000000000000000010,
9+
const A_REQUIRED_ELSE_HELP = 0b00000000000000000000000000100,
10+
const GLOBAL_VERSION = 0b00000000000000000000000001000,
11+
const VERSIONLESS_SC = 0b00000000000000000000000010000,
12+
const UNIFIED_HELP = 0b00000000000000000000000100000,
13+
const WAIT_ON_ERROR = 0b00000000000000000000001000000,
14+
const SC_REQUIRED_ELSE_HELP= 0b00000000000000000000010000000,
15+
const NEEDS_LONG_HELP = 0b00000000000000000000100000000,
16+
const NEEDS_LONG_VERSION = 0b00000000000000000001000000000,
17+
const NEEDS_SC_HELP = 0b00000000000000000010000000000,
18+
const DISABLE_VERSION = 0b00000000000000000100000000000,
19+
const HIDDEN = 0b00000000000000001000000000000,
20+
const TRAILING_VARARG = 0b00000000000000010000000000000,
21+
const NO_BIN_NAME = 0b00000000000000100000000000000,
22+
const ALLOW_UNK_SC = 0b00000000000001000000000000000,
23+
const UTF8_STRICT = 0b00000000000010000000000000000,
24+
const UTF8_NONE = 0b00000000000100000000000000000,
25+
const LEADING_HYPHEN = 0b00000000001000000000000000000,
26+
const NO_POS_VALUES = 0b00000000010000000000000000000,
27+
const NEXT_LINE_HELP = 0b00000000100000000000000000000,
28+
const DERIVE_DISP_ORDER = 0b00000001000000000000000000000,
29+
const COLORED_HELP = 0b00000010000000000000000000000,
30+
const COLOR_ALWAYS = 0b00000100000000000000000000000,
31+
const COLOR_AUTO = 0b00001000000000000000000000000,
32+
const COLOR_NEVER = 0b00010000000000000000000000000,
33+
const DONT_DELIM_TRAIL = 0b00100000000000000000000000000,
34+
const ALLOW_NEG_NUMS = 0b01000000000000000000000000000,
35+
const LOW_INDEX_MUL_POS = 0b10000000000000000000000000000,
3536
}
3637
}
3738

@@ -72,6 +73,7 @@ impl AppFlags {
7273
GlobalVersion => GLOBAL_VERSION,
7374
HidePossibleValuesInHelp => NO_POS_VALUES,
7475
Hidden => HIDDEN,
76+
LowIndexMultiplePositional => LOW_INDEX_MUL_POS,
7577
NeedsLongHelp => NEEDS_LONG_HELP,
7678
NeedsLongVersion => NEEDS_LONG_VERSION,
7779
NeedsSubcommandHelp => NEEDS_SC_HELP,
@@ -663,6 +665,9 @@ pub enum AppSettings {
663665
#[doc(hidden)]
664666
NeedsSubcommandHelp,
665667

668+
#[doc(hidden)]
669+
LowIndexMultiplePositional,
670+
666671
}
667672

668673
impl FromStr for AppSettings {
@@ -684,6 +689,7 @@ impl FromStr for AppSettings {
684689
"globalversion" => Ok(AppSettings::GlobalVersion),
685690
"hidden" => Ok(AppSettings::Hidden),
686691
"hidepossiblevaluesinhelp" => Ok(AppSettings::HidePossibleValuesInHelp),
692+
"lowindexmultiplepositional" => Ok(AppSettings::LowIndexMultiplePositional),
687693
"nobinaryname" => Ok(AppSettings::NoBinaryName),
688694
"nextlinehelp" => Ok(AppSettings::NextLineHelp),
689695
"strictutf8" => Ok(AppSettings::StrictUtf8),
@@ -735,6 +741,8 @@ mod test {
735741
AppSettings::Hidden);
736742
assert_eq!("hidepossiblevaluesinhelp".parse::<AppSettings>().unwrap(),
737743
AppSettings::HidePossibleValuesInHelp);
744+
assert_eq!("lowindexmultiplePositional".parse::<AppSettings>().unwrap(),
745+
AppSettings::LowIndexMultiplePositional);
738746
assert_eq!("nobinaryname".parse::<AppSettings>().unwrap(),
739747
AppSettings::NoBinaryName);
740748
assert_eq!("nextlinehelp".parse::<AppSettings>().unwrap(),

0 commit comments

Comments
 (0)