Skip to content

Commit d6c3983

Browse files
committed
Auto merge of #5196 - matklad:clapclapclap, r=alexcrichton
Fix a regression with parsing multivalue options By default, clap interprets ``` cargo run --bin foo bar baz ``` as ``` cargo run --bin foo --bin bar --bin baz ``` This behavior is different from docopt and does not play nicely with positional arguments at all. Luckily, clap has a flag to get the behavior we want, it just not the default! It will become the default in the next version of clap, but, until that time, we should be careful when using the combination of `.long`, `.value_name` and `.multiple(true)`, and don't forget to specify `.number_of_values(1)` as well. @alexcrichton I'd love to merge this fix before updating cargo at rust-lang/rust :)
2 parents 1184854 + 70ff33a commit d6c3983

File tree

16 files changed

+89
-53
lines changed

16 files changed

+89
-53
lines changed

src/bin/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ See 'cargo help <command>' for more information on a specific command.",
165165
.short("Z")
166166
.value_name("FLAG")
167167
.multiple(true)
168+
.number_of_values(1)
168169
.global(true),
169170
)
170171
.subcommands(commands::builtin());

src/bin/command_prelude.rs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,22 @@ pub type App = clap::App<'static, 'static>;
1515
pub trait AppExt: Sized {
1616
fn _arg(self, arg: Arg<'static, 'static>) -> Self;
1717

18-
fn arg_package(self, package: &'static str, all: &'static str, exclude: &'static str) -> Self {
19-
self._arg(
20-
opt("package", package)
21-
.short("p")
22-
.value_name("SPEC")
23-
.multiple(true),
24-
)._arg(opt("all", all))
25-
._arg(opt("exclude", exclude).value_name("SPEC").multiple(true))
18+
fn arg_package_spec(
19+
self,
20+
package: &'static str,
21+
all: &'static str,
22+
exclude: &'static str,
23+
) -> Self {
24+
self.arg_package_spec_simple(package)
25+
._arg(opt("all", all))
26+
._arg(multi_opt("exclude", "SPEC", exclude))
2627
}
2728

28-
fn arg_single_package(self, package: &'static str) -> Self {
29+
fn arg_package_spec_simple(self, package: &'static str) -> Self {
30+
self._arg(multi_opt("package", "SPEC", package).short("p"))
31+
}
32+
33+
fn arg_package(self, package: &'static str) -> Self {
2934
self._arg(opt("package", package).short("p").value_name("SPEC"))
3035
}
3136

@@ -51,18 +56,18 @@ pub trait AppExt: Sized {
5156
all: &'static str,
5257
) -> Self {
5358
self.arg_targets_lib_bin(lib, bin, bins)
54-
._arg(opt("example", examle).value_name("NAME").multiple(true))
59+
._arg(multi_opt("example", "NAME", examle))
5560
._arg(opt("examples", examles))
56-
._arg(opt("test", test).value_name("NAME").multiple(true))
61+
._arg(multi_opt("test", "NAME", test))
5762
._arg(opt("tests", tests))
58-
._arg(opt("bench", bench).value_name("NAME").multiple(true))
63+
._arg(multi_opt("bench", "NAME", bench))
5964
._arg(opt("benches", benchs))
6065
._arg(opt("all-targets", all))
6166
}
6267

6368
fn arg_targets_lib_bin(self, lib: &'static str, bin: &'static str, bins: &'static str) -> Self {
6469
self._arg(opt("lib", lib))
65-
._arg(opt("bin", bin).value_name("NAME").multiple(true))
70+
._arg(multi_opt("bin", "NAME", bin))
6671
._arg(opt("bins", bins))
6772
}
6873

@@ -73,15 +78,15 @@ pub trait AppExt: Sized {
7378
example: &'static str,
7479
examples: &'static str,
7580
) -> Self {
76-
self._arg(opt("bin", bin).value_name("NAME").multiple(true))
81+
self._arg(multi_opt("bin", "NAME", bin))
7782
._arg(opt("bins", bins))
78-
._arg(opt("example", example).value_name("NAME").multiple(true))
83+
._arg(multi_opt("example", "NAME", example))
7984
._arg(opt("examples", examples))
8085
}
8186

8287
fn arg_targets_bin_example(self, bin: &'static str, example: &'static str) -> Self {
83-
self._arg(opt("bin", bin).value_name("NAME").multiple(true))
84-
._arg(opt("example", example).value_name("NAME").multiple(true))
88+
self._arg(multi_opt("bin", "NAME", bin))
89+
._arg(multi_opt("example", "NAME", example))
8590
}
8691

8792
fn arg_features(self) -> Self {
@@ -157,6 +162,21 @@ pub fn opt(name: &'static str, help: &'static str) -> Arg<'static, 'static> {
157162
Arg::with_name(name).long(name).help(help)
158163
}
159164

165+
pub fn multi_opt(
166+
name: &'static str,
167+
value_name: &'static str,
168+
help: &'static str,
169+
) -> Arg<'static, 'static> {
170+
// Note that all `.multiple(true)` arguments in Cargo should specify
171+
// `.number_of_values(1)` as well, so that `--foo val1 val2` is
172+
// **not** parsed as `foo` with values ["val1", "val2"].
173+
// `number_of_values` should become the default in clap 3.
174+
opt(name, help)
175+
.value_name(value_name)
176+
.multiple(true)
177+
.number_of_values(1)
178+
}
179+
160180
pub fn subcommand(name: &'static str) -> App {
161181
SubCommand::with_name(name).settings(&[
162182
AppSettings::UnifiedHelpMessage,

src/bin/commands/bench.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub fn cli() -> App {
2929
"Benchmark all targets (default)",
3030
)
3131
.arg(opt("no-run", "Compile, but don't run benchmarks"))
32-
.arg_package(
32+
.arg_package_spec(
3333
"Package to run benchmarks for",
3434
"Benchmark all packages in the workspace",
3535
"Exclude packages from the benchmark",

src/bin/commands/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub fn cli() -> App {
66
subcommand("build")
77
.alias("b")
88
.about("Compile a local package and all of its dependencies")
9-
.arg_package(
9+
.arg_package_spec(
1010
"Package to build",
1111
"Build all packages in the workspace",
1212
"Exclude packages from the build",

src/bin/commands/check.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use cargo::ops::{self, CompileMode};
55
pub fn cli() -> App {
66
subcommand("check")
77
.about("Check a local package and all of its dependencies for errors")
8-
.arg_package(
8+
.arg_package_spec(
99
"Package(s) to check",
1010
"Check all packages in the workspace",
1111
"Exclude packages from the check",

src/bin/commands/clean.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ use cargo::ops::{self, CleanOptions};
55
pub fn cli() -> App {
66
subcommand("clean")
77
.about("Remove artifacts that cargo has generated in the past")
8-
.arg(
9-
opt("package", "Package to clean artifacts for")
10-
.short("p")
11-
.value_name("SPEC")
12-
.multiple(true),
13-
)
8+
.arg_package_spec_simple("Package to clean artifacts for")
149
.arg_manifest_path()
1510
.arg_target_triple("Target triple to clean output for (default all)")
1611
.arg_release("Whether or not to clean release artifacts")

src/bin/commands/doc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub fn cli() -> App {
99
"open",
1010
"Opens the docs in a browser after the operation",
1111
))
12-
.arg_package(
12+
.arg_package_spec(
1313
"Package to document",
1414
"Document all packages in the workspace",
1515
"Exclude packages from the build",

src/bin/commands/owner.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,13 @@ pub fn cli() -> App {
66
subcommand("owner")
77
.about("Manage the owners of a crate on the registry")
88
.arg(Arg::with_name("crate"))
9+
.arg(multi_opt("add", "LOGIN", "Name of a user or team to add as an owner").short("a"))
910
.arg(
10-
opt("add", "Name of a user or team to add as an owner")
11-
.short("a")
12-
.value_name("LOGIN")
13-
.multiple(true),
14-
)
15-
.arg(
16-
opt("remove", "Name of a user or team to remove as an owner")
17-
.short("r")
18-
.value_name("LOGIN")
19-
.multiple(true),
11+
multi_opt(
12+
"remove",
13+
"LOGIN",
14+
"Name of a user or team to remove as an owner",
15+
).short("r"),
2016
)
2117
.arg(opt("list", "List owners of a crate").short("l"))
2218
.arg(opt("index", "Registry index to modify owners for").value_name("INDEX"))

src/bin/commands/pkgid.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub fn cli() -> App {
66
subcommand("pkgid")
77
.about("Print a fully qualified package specification")
88
.arg(Arg::with_name("spec"))
9-
.arg_single_package("Argument to get the package id specifier for")
9+
.arg_package("Argument to get the package id specifier for")
1010
.arg_manifest_path()
1111
.after_help(
1212
"\

src/bin/commands/run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub fn cli() -> App {
1313
"Name of the bin target to run",
1414
"Name of the example target to run",
1515
)
16-
.arg_single_package("Package with the target to run")
16+
.arg_package("Package with the target to run")
1717
.arg_jobs()
1818
.arg_release("Build artifacts in release mode, with optimizations")
1919
.arg_features()

src/bin/commands/rustc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub fn cli() -> App {
77
.setting(AppSettings::TrailingVarArg)
88
.about("Compile a package and all of its dependencies")
99
.arg(Arg::with_name("args").multiple(true))
10-
.arg_single_package("Package to build")
10+
.arg_package("Package to build")
1111
.arg_jobs()
1212
.arg_targets_all(
1313
"Build only this package's library",

src/bin/commands/rustdoc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub fn cli() -> App {
1111
"open",
1212
"Opens the docs in a browser after the operation",
1313
))
14-
.arg_single_package("Package to document")
14+
.arg_package("Package to document")
1515
.arg_jobs()
1616
.arg_targets_all(
1717
"Build only this package's library",

src/bin/commands/test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn cli() -> App {
3232
.arg(opt("doc", "Test only this library's documentation"))
3333
.arg(opt("no-run", "Compile, but don't run tests"))
3434
.arg(opt("no-fail-fast", "Run all tests regardless of failure"))
35-
.arg_package(
35+
.arg_package_spec(
3636
"Package to run tests for",
3737
"Test all packages in the workspace",
3838
"Exclude packages from the test",

src/bin/commands/uninstall.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ pub fn cli() -> App {
66
subcommand("uninstall")
77
.about("Remove a Rust binary")
88
.arg(Arg::with_name("spec").multiple(true))
9-
.arg(
10-
opt("bin", "Only uninstall the binary NAME")
11-
.value_name("NAME")
12-
.multiple(true),
13-
)
9+
.arg(multi_opt("bin", "NAME", "Only uninstall the binary NAME"))
1410
.arg(opt("root", "Directory to uninstall packages from").value_name("DIR"))
1511
.after_help(
1612
"\

src/bin/commands/update.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ use cargo::ops::{self, UpdateOptions};
55
pub fn cli() -> App {
66
subcommand("update")
77
.about("Update dependencies as recorded in the local lock file")
8-
.arg(
9-
opt("package", "Package to clean artifacts for")
10-
.short("p")
11-
.value_name("SPEC")
12-
.multiple(true),
13-
)
8+
.arg_package_spec_simple("Package to update")
149
.arg(opt(
1510
"aggressive",
1611
"Force updating all dependencies of <name> as well",

tests/testsuite/run.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,3 +1109,36 @@ fn run_multiple_packages() {
11091109
.with_stderr_contains("[ERROR] package `d3` is not a member of the workspace"),
11101110
);
11111111
}
1112+
1113+
#[test]
1114+
fn explicit_bin_with_args() {
1115+
let p = project("foo")
1116+
.file(
1117+
"Cargo.toml",
1118+
r#"
1119+
[project]
1120+
name = "foo"
1121+
version = "0.0.1"
1122+
authors = []
1123+
"#,
1124+
)
1125+
.file(
1126+
"src/main.rs",
1127+
r#"
1128+
fn main() {
1129+
assert_eq!(std::env::args().nth(1).unwrap(), "hello");
1130+
assert_eq!(std::env::args().nth(2).unwrap(), "world");
1131+
}
1132+
"#,
1133+
)
1134+
.build();
1135+
1136+
assert_that(
1137+
p.cargo("run")
1138+
.arg("--bin")
1139+
.arg("foo")
1140+
.arg("hello")
1141+
.arg("world"),
1142+
execs().with_status(0),
1143+
);
1144+
}

0 commit comments

Comments
 (0)