Skip to content

Commit cfc019d

Browse files
committed
feat: dis/allow -- as value
1 parent eabbc6b commit cfc019d

File tree

9 files changed

+243
-3
lines changed

9 files changed

+243
-3
lines changed

clap_builder/src/builder/arg.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,19 @@ impl Arg {
15461546
}
15471547
}
15481548

1549+
/// Allow a bare `--` to be treated as a value when parsing.
1550+
///
1551+
/// Defaults to `true` to preserve existing `allow_hyphen_values` behavior.
1552+
#[inline]
1553+
#[must_use]
1554+
pub fn allow_dash_dash_as_value(self, yes: bool) -> Self {
1555+
if yes {
1556+
self.unset_setting(ArgSettings::ForbidDashDashAsValue)
1557+
} else {
1558+
self.setting(ArgSettings::ForbidDashDashAsValue)
1559+
}
1560+
}
1561+
15491562
/// Allows negative numbers to pass as values.
15501563
///
15511564
/// This is similar to [`Arg::allow_hyphen_values`] except that it only allows numbers,
@@ -4449,6 +4462,11 @@ impl Arg {
44494462
self.is_set(ArgSettings::AllowHyphenValues)
44504463
}
44514464

4465+
/// Report whether [`Arg::allow_dash_dash_as_value`] is set
4466+
pub fn is_allow_dash_dash_as_value_set(&self) -> bool {
4467+
!self.is_set(ArgSettings::ForbidDashDashAsValue)
4468+
}
4469+
44524470
/// Report whether [`Arg::allow_negative_numbers`] is set
44534471
pub fn is_allow_negative_numbers_set(&self) -> bool {
44544472
self.is_set(ArgSettings::AllowNegativeNumbers)

clap_builder/src/builder/arg_settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub(crate) enum ArgSettings {
4949
HidePossibleValues,
5050
AllowHyphenValues,
5151
AllowNegativeNumbers,
52+
ForbidDashDashAsValue,
5253
RequireEquals,
5354
Last,
5455
TrailingVarArg,

clap_builder/src/parser/parser.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,22 @@ impl<'cmd> Parser<'cmd> {
128128
}
129129

130130
if arg_os.is_escape() {
131-
if matches!(&parse_state, ParseState::Opt(opt) | ParseState::Pos(opt) if
132-
self.cmd[opt].is_allow_hyphen_values_set())
131+
if matches!(&parse_state, ParseState::Opt(opt) | ParseState::Pos(opt)
132+
if self.cmd[opt].is_allow_hyphen_values_set()
133+
&& self.cmd[opt].is_allow_dash_dash_as_value_set())
133134
{
134135
// ParseResult::MaybeHyphenValue, do nothing
135136
} else {
137+
if let ParseState::Opt(ref opt) = parse_state {
138+
let arg = &self.cmd[opt];
139+
if arg.get_num_args().expect(INTERNAL_ERROR_MSG).min_values() > 0 {
140+
return Err(ClapError::missing_required_argument(
141+
self.cmd,
142+
vec![arg.to_string()],
143+
Usage::new(self.cmd).create_usage_with_title(&[]),
144+
));
145+
}
146+
}
136147
debug!("Parser::get_matches_with: setting TrailingVals=true");
137148
trailing_values = true;
138149
matcher.start_trailing();

clap_complete/src/engine/complete.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ pub fn complete(
6969
(next_state, pos_index) =
7070
parse_positional(current_cmd, pos_index, is_escaped, current_state);
7171
} else if arg.is_escape() {
72+
if let ParseState::Opt((opt, count)) = current_state {
73+
if opt.is_allow_hyphen_values_set() && opt.is_allow_dash_dash_as_value_set() {
74+
next_state = parse_opt_value(opt, count);
75+
continue;
76+
}
77+
}
7278
is_escaped = true;
7379
} else if opt_allows_hyphen(&current_state, &arg) {
7480
match current_state {

clap_complete/tests/testsuite/common.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,24 @@ pub(crate) fn optional_multi_value_option_command(name: &'static str) -> clap::C
300300
)
301301
}
302302

303+
#[allow(dead_code)]
304+
pub(crate) fn allow_dash_dash_as_value_command(name: &'static str) -> clap::Command {
305+
clap::Command::new(name)
306+
.arg(
307+
clap::Arg::new("value")
308+
.long("value")
309+
.action(clap::ArgAction::Set)
310+
.allow_hyphen_values(true)
311+
.allow_dash_dash_as_value(false),
312+
)
313+
.arg(
314+
clap::Arg::new("pos")
315+
.help("collect trailing values")
316+
.num_args(0..)
317+
.last(true),
318+
)
319+
}
320+
303321
pub(crate) fn two_multi_valued_arguments_command(name: &'static str) -> clap::Command {
304322
clap::Command::new(name)
305323
.arg(

clap_complete/tests/testsuite/engine.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,26 @@ pos_c
378378
assert_data_eq!(complete!(cmd, "-cSF=[TAB]"), snapbox::str![]);
379379
}
380380

381+
#[test]
382+
fn terminator_after_allow_dash_dash_as_value_false() {
383+
let mut cmd = Command::new("exhaustive")
384+
.arg(
385+
clap::Arg::new("value")
386+
.long("value")
387+
.action(clap::ArgAction::Set)
388+
.allow_hyphen_values(true)
389+
.allow_dash_dash_as_value(false),
390+
)
391+
.arg(
392+
clap::Arg::new("pos")
393+
.value_parser(["rest"])
394+
.num_args(0..)
395+
.last(true),
396+
);
397+
398+
assert_data_eq!(complete!(cmd, "--value -- r"), snapbox::str!["rest"]);
399+
}
400+
381401
#[test]
382402
fn suggest_argument_multi_values() {
383403
let mut cmd = Command::new("dynamic")

tests/builder/opts.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,116 @@ fn double_hyphen_as_value() {
7171
);
7272
}
7373

74+
#[test]
75+
fn double_hyphen_not_a_value_when_disabled() {
76+
let res = Command::new("prog")
77+
.arg(
78+
Arg::new("value")
79+
.action(ArgAction::Set)
80+
.allow_hyphen_values(true)
81+
.allow_dash_dash_as_value(false)
82+
.long("value"),
83+
)
84+
.try_get_matches_from(vec!["prog", "--value", "--"]);
85+
assert!(res.is_err());
86+
assert_eq!(res.unwrap_err().kind(), ErrorKind::MissingRequiredArgument);
87+
}
88+
89+
#[test]
90+
fn double_hyphen_as_terminator_after_flag() {
91+
let matches = Command::new("prog")
92+
.arg(
93+
Arg::new("flag")
94+
.action(ArgAction::SetTrue)
95+
.long("flag"),
96+
)
97+
.try_get_matches_from(vec!["prog", "--flag", "--"])
98+
.unwrap();
99+
100+
assert_eq!(matches.get_one::<bool>("flag"), Some(&true));
101+
}
102+
103+
#[test]
104+
fn double_hyphen_as_terminator_between_two_flags() {
105+
let matches = Command::new("prog")
106+
.arg(Arg::new("first").action(ArgAction::SetTrue).long("first"))
107+
.arg(Arg::new("second").action(ArgAction::SetTrue).long("second"))
108+
.try_get_matches_from(vec!["prog", "--first", "--", "--second"]);
109+
110+
assert!(matches.is_err());
111+
assert_eq!(matches.unwrap_err().kind(), ErrorKind::UnknownArgument);
112+
}
113+
114+
#[test]
115+
fn double_hyphen_as_terminator_between_two_flags_before_last_positional() {
116+
let matches = Command::new("prog")
117+
.arg(Arg::new("first").action(ArgAction::SetTrue).long("first"))
118+
.arg(Arg::new("second").action(ArgAction::SetTrue).long("second"))
119+
.arg(Arg::new("remaining").num_args(0..).last(true))
120+
.try_get_matches_from(vec!["prog", "--first", "--", "--second"])
121+
.unwrap();
122+
123+
assert_eq!(matches.get_one::<bool>("first"), Some(&true));
124+
assert_eq!(matches.get_one::<bool>("second"), Some(&false));
125+
let remaining: Vec<_> = matches
126+
.get_many::<String>("remaining")
127+
.into_iter()
128+
.flatten()
129+
.map(|s| s.as_str())
130+
.collect();
131+
assert_eq!(remaining, ["--second"]);
132+
}
133+
134+
#[test]
135+
fn double_hyphen_as_terminator_between_two_flags_before_last_positional_reversed() {
136+
let matches = Command::new("prog")
137+
.arg(Arg::new("first").action(ArgAction::SetTrue).long("first"))
138+
.arg(Arg::new("second").action(ArgAction::SetTrue).long("second"))
139+
.arg(Arg::new("remaining").num_args(0..).last(true))
140+
.try_get_matches_from(vec!["prog", "--second", "--", "--first"])
141+
.unwrap();
142+
143+
assert_eq!(matches.get_one::<bool>("first"), Some(&false));
144+
assert_eq!(matches.get_one::<bool>("second"), Some(&true));
145+
let remaining: Vec<_> = matches
146+
.get_many::<String>("remaining")
147+
.into_iter()
148+
.flatten()
149+
.map(|s| s.as_str())
150+
.collect();
151+
assert_eq!(remaining, ["--first"]);
152+
}
153+
154+
#[test]
155+
fn double_hyphen_as_terminator_between_two_opts_before_last_positional() {
156+
let matches = Command::new("prog")
157+
.arg(
158+
Arg::new("first")
159+
.action(ArgAction::Set)
160+
.allow_hyphen_values(true)
161+
.long("first"),
162+
)
163+
.arg(
164+
Arg::new("second")
165+
.action(ArgAction::Set)
166+
.allow_hyphen_values(true)
167+
.long("second"),
168+
)
169+
.arg(Arg::new("remaining").num_args(0..).last(true))
170+
.try_get_matches_from(vec!["prog", "--second", "v2", "--", "--first", "v1"])
171+
.unwrap();
172+
173+
assert_eq!(matches.get_one::<String>("first").map(|s| s.as_str()), None);
174+
assert_eq!(matches.get_one::<String>("second").map(|s| s.as_str()), Some("v2"));
175+
let remaining: Vec<_> = matches
176+
.get_many::<String>("remaining")
177+
.into_iter()
178+
.flatten()
179+
.map(|s| s.as_str())
180+
.collect();
181+
assert_eq!(remaining, ["--first", "v1"]);
182+
}
183+
74184
#[test]
75185
fn require_equals_no_empty_values_fail() {
76186
let res = Command::new("prog")

tests/builder/positionals.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,42 @@ fn ignore_hyphen_values_on_last() {
350350
Some("foo")
351351
);
352352
}
353+
354+
#[test]
355+
fn double_dash_splits_when_not_allowed_as_value() {
356+
let cmd = Command::new("prog")
357+
.arg(
358+
Arg::new("before")
359+
.action(ArgAction::Set)
360+
.num_args(0..)
361+
.allow_hyphen_values(true)
362+
.allow_dash_dash_as_value(false),
363+
)
364+
.arg(
365+
Arg::new("after")
366+
.action(ArgAction::Set)
367+
.num_args(0..)
368+
.last(true)
369+
.allow_hyphen_values(true),
370+
);
371+
372+
let matches = cmd
373+
.try_get_matches_from(["prog", "--release", "--", "--expand-errors", "--rlimit=100"])
374+
.unwrap();
375+
376+
let before: Vec<_> = matches
377+
.get_many::<String>("before")
378+
.into_iter()
379+
.flatten()
380+
.map(|s| s.as_str())
381+
.collect();
382+
assert_eq!(before, vec!["--release"]);
383+
384+
let after: Vec<_> = matches
385+
.get_many::<String>("after")
386+
.into_iter()
387+
.flatten()
388+
.map(|s| s.as_str())
389+
.collect();
390+
assert_eq!(after, vec!["--expand-errors", "--rlimit=100"]);
391+
}

tests/derive/options.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
#![allow(clippy::option_option)]
1616

17-
use clap::{Parser, Subcommand};
17+
use clap::{error::ErrorKind, Parser, Subcommand};
1818
use snapbox::assert_data_eq;
1919
use snapbox::prelude::*;
2020
use snapbox::str;
@@ -548,3 +548,20 @@ fn implicit_value_parser() {
548548
Opt::try_parse_from(["test", "--arg", "42"]).unwrap()
549549
);
550550
}
551+
552+
#[test]
553+
fn allow_dash_dash_as_value_false_via_attr() {
554+
#[derive(Debug, Parser, PartialEq)]
555+
struct Opt {
556+
#[arg(
557+
long,
558+
allow_hyphen_values = true,
559+
allow_dash_dash_as_value = false
560+
)]
561+
value: String,
562+
}
563+
564+
let res = Opt::try_parse_from(["test", "--value", "--"]);
565+
assert!(res.is_err());
566+
assert_eq!(res.unwrap_err().kind(), ErrorKind::MissingRequiredArgument);
567+
}

0 commit comments

Comments
 (0)