Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions clap_builder/src/builder/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,19 @@ impl Arg {
}
}

/// Allow a bare `--` to be treated as a value when parsing.
///
/// Defaults to `true` to preserve existing `allow_hyphen_values` behavior.
#[inline]
#[must_use]
pub fn allow_dash_dash_as_value(self, yes: bool) -> Self {
if yes {
self.unset_setting(ArgSettings::ForbidDashDashAsValue)
} else {
self.setting(ArgSettings::ForbidDashDashAsValue)
}
}

/// Allows negative numbers to pass as values.
///
/// This is similar to [`Arg::allow_hyphen_values`] except that it only allows numbers,
Expand Down Expand Up @@ -4449,6 +4462,11 @@ impl Arg {
self.is_set(ArgSettings::AllowHyphenValues)
}

/// Report whether [`Arg::allow_dash_dash_as_value`] is set
pub fn is_allow_dash_dash_as_value_set(&self) -> bool {
!self.is_set(ArgSettings::ForbidDashDashAsValue)
}

/// Report whether [`Arg::allow_negative_numbers`] is set
pub fn is_allow_negative_numbers_set(&self) -> bool {
self.is_set(ArgSettings::AllowNegativeNumbers)
Expand Down
1 change: 1 addition & 0 deletions clap_builder/src/builder/arg_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub(crate) enum ArgSettings {
HidePossibleValues,
AllowHyphenValues,
AllowNegativeNumbers,
ForbidDashDashAsValue,
RequireEquals,
Last,
TrailingVarArg,
Expand Down
15 changes: 13 additions & 2 deletions clap_builder/src/parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,22 @@ impl<'cmd> Parser<'cmd> {
}

if arg_os.is_escape() {
if matches!(&parse_state, ParseState::Opt(opt) | ParseState::Pos(opt) if
self.cmd[opt].is_allow_hyphen_values_set())
if matches!(&parse_state, ParseState::Opt(opt) | ParseState::Pos(opt)
if self.cmd[opt].is_allow_hyphen_values_set()
&& self.cmd[opt].is_allow_dash_dash_as_value_set())
{
// ParseResult::MaybeHyphenValue, do nothing
} else {
if let ParseState::Opt(ref opt) = parse_state {
let arg = &self.cmd[opt];
if arg.get_num_args().expect(INTERNAL_ERROR_MSG).min_values() > 0 {
return Err(ClapError::missing_required_argument(
self.cmd,
vec![arg.to_string()],
Usage::new(self.cmd).create_usage_with_title(&[]),
));
}
}
debug!("Parser::get_matches_with: setting TrailingVals=true");
trailing_values = true;
matcher.start_trailing();
Expand Down
6 changes: 6 additions & 0 deletions clap_complete/src/engine/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ pub fn complete(
(next_state, pos_index) =
parse_positional(current_cmd, pos_index, is_escaped, current_state);
} else if arg.is_escape() {
if let ParseState::Opt((opt, count)) = current_state {
if opt.is_allow_hyphen_values_set() && opt.is_allow_dash_dash_as_value_set() {
next_state = parse_opt_value(opt, count);
continue;
}
}
is_escaped = true;
} else if opt_allows_hyphen(&current_state, &arg) {
match current_state {
Expand Down
18 changes: 18 additions & 0 deletions clap_complete/tests/testsuite/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,24 @@ pub(crate) fn optional_multi_value_option_command(name: &'static str) -> clap::C
)
}

#[allow(dead_code)]
pub(crate) fn allow_dash_dash_as_value_command(name: &'static str) -> clap::Command {
clap::Command::new(name)
.arg(
clap::Arg::new("value")
.long("value")
.action(clap::ArgAction::Set)
.allow_hyphen_values(true)
.allow_dash_dash_as_value(false),
)
.arg(
clap::Arg::new("pos")
.help("collect trailing values")
.num_args(0..)
.last(true),
)
}

pub(crate) fn two_multi_valued_arguments_command(name: &'static str) -> clap::Command {
clap::Command::new(name)
.arg(
Expand Down
20 changes: 20 additions & 0 deletions clap_complete/tests/testsuite/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,26 @@ pos_c
assert_data_eq!(complete!(cmd, "-cSF=[TAB]"), snapbox::str![]);
}

#[test]
fn terminator_after_allow_dash_dash_as_value_false() {
let mut cmd = Command::new("exhaustive")
.arg(
clap::Arg::new("value")
.long("value")
.action(clap::ArgAction::Set)
.allow_hyphen_values(true)
.allow_dash_dash_as_value(false),
)
.arg(
clap::Arg::new("pos")
.value_parser(["rest"])
.num_args(0..)
.last(true),
);

assert_data_eq!(complete!(cmd, "--value -- r"), snapbox::str!["rest"]);
}

#[test]
fn suggest_argument_multi_values() {
let mut cmd = Command::new("dynamic")
Expand Down
110 changes: 110 additions & 0 deletions tests/builder/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,116 @@ fn double_hyphen_as_value() {
);
}

#[test]
fn double_hyphen_not_a_value_when_disabled() {
let res = Command::new("prog")
.arg(
Arg::new("value")
.action(ArgAction::Set)
.allow_hyphen_values(true)
.allow_dash_dash_as_value(false)
.long("value"),
)
.try_get_matches_from(vec!["prog", "--value", "--"]);
assert!(res.is_err());
assert_eq!(res.unwrap_err().kind(), ErrorKind::MissingRequiredArgument);
}

#[test]
fn double_hyphen_as_terminator_after_flag() {
let matches = Command::new("prog")
.arg(
Arg::new("flag")
.action(ArgAction::SetTrue)
.long("flag"),
)
.try_get_matches_from(vec!["prog", "--flag", "--"])
.unwrap();

assert_eq!(matches.get_one::<bool>("flag"), Some(&true));
}

#[test]
fn double_hyphen_as_terminator_between_two_flags() {
let matches = Command::new("prog")
.arg(Arg::new("first").action(ArgAction::SetTrue).long("first"))
.arg(Arg::new("second").action(ArgAction::SetTrue).long("second"))
.try_get_matches_from(vec!["prog", "--first", "--", "--second"]);

assert!(matches.is_err());
assert_eq!(matches.unwrap_err().kind(), ErrorKind::UnknownArgument);
}

#[test]
fn double_hyphen_as_terminator_between_two_flags_before_last_positional() {
let matches = Command::new("prog")
.arg(Arg::new("first").action(ArgAction::SetTrue).long("first"))
.arg(Arg::new("second").action(ArgAction::SetTrue).long("second"))
.arg(Arg::new("remaining").num_args(0..).last(true))
.try_get_matches_from(vec!["prog", "--first", "--", "--second"])
.unwrap();

assert_eq!(matches.get_one::<bool>("first"), Some(&true));
assert_eq!(matches.get_one::<bool>("second"), Some(&false));
let remaining: Vec<_> = matches
.get_many::<String>("remaining")
.into_iter()
.flatten()
.map(|s| s.as_str())
.collect();
assert_eq!(remaining, ["--second"]);
}

#[test]
fn double_hyphen_as_terminator_between_two_flags_before_last_positional_reversed() {
let matches = Command::new("prog")
.arg(Arg::new("first").action(ArgAction::SetTrue).long("first"))
.arg(Arg::new("second").action(ArgAction::SetTrue).long("second"))
.arg(Arg::new("remaining").num_args(0..).last(true))
.try_get_matches_from(vec!["prog", "--second", "--", "--first"])
.unwrap();

assert_eq!(matches.get_one::<bool>("first"), Some(&false));
assert_eq!(matches.get_one::<bool>("second"), Some(&true));
let remaining: Vec<_> = matches
.get_many::<String>("remaining")
.into_iter()
.flatten()
.map(|s| s.as_str())
.collect();
assert_eq!(remaining, ["--first"]);
}

#[test]
fn double_hyphen_as_terminator_between_two_opts_before_last_positional() {
let matches = Command::new("prog")
.arg(
Arg::new("first")
.action(ArgAction::Set)
.allow_hyphen_values(true)
.long("first"),
)
.arg(
Arg::new("second")
.action(ArgAction::Set)
.allow_hyphen_values(true)
.long("second"),
)
.arg(Arg::new("remaining").num_args(0..).last(true))
.try_get_matches_from(vec!["prog", "--second", "v2", "--", "--first", "v1"])
.unwrap();

assert_eq!(matches.get_one::<String>("first").map(|s| s.as_str()), None);
assert_eq!(matches.get_one::<String>("second").map(|s| s.as_str()), Some("v2"));
let remaining: Vec<_> = matches
.get_many::<String>("remaining")
.into_iter()
.flatten()
.map(|s| s.as_str())
.collect();
assert_eq!(remaining, ["--first", "v1"]);
}

#[test]
fn require_equals_no_empty_values_fail() {
let res = Command::new("prog")
Expand Down
39 changes: 39 additions & 0 deletions tests/builder/positionals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,42 @@ fn ignore_hyphen_values_on_last() {
Some("foo")
);
}

#[test]
fn double_dash_splits_when_not_allowed_as_value() {
let cmd = Command::new("prog")
.arg(
Arg::new("before")
.action(ArgAction::Set)
.num_args(0..)
.allow_hyphen_values(true)
.allow_dash_dash_as_value(false),
)
.arg(
Arg::new("after")
.action(ArgAction::Set)
.num_args(0..)
.last(true)
.allow_hyphen_values(true),
);

let matches = cmd
.try_get_matches_from(["prog", "--release", "--", "--expand-errors", "--rlimit=100"])
.unwrap();

let before: Vec<_> = matches
.get_many::<String>("before")
.into_iter()
.flatten()
.map(|s| s.as_str())
.collect();
assert_eq!(before, vec!["--release"]);

let after: Vec<_> = matches
.get_many::<String>("after")
.into_iter()
.flatten()
.map(|s| s.as_str())
.collect();
assert_eq!(after, vec!["--expand-errors", "--rlimit=100"]);
}
19 changes: 18 additions & 1 deletion tests/derive/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#![allow(clippy::option_option)]

use clap::{Parser, Subcommand};
use clap::{error::ErrorKind, Parser, Subcommand};
use snapbox::assert_data_eq;
use snapbox::prelude::*;
use snapbox::str;
Expand Down Expand Up @@ -548,3 +548,20 @@ fn implicit_value_parser() {
Opt::try_parse_from(["test", "--arg", "42"]).unwrap()
);
}

#[test]
fn allow_dash_dash_as_value_false_via_attr() {
#[derive(Debug, Parser, PartialEq)]
struct Opt {
#[arg(
long,
allow_hyphen_values = true,
allow_dash_dash_as_value = false
)]
value: String,
}

let res = Opt::try_parse_from(["test", "--value", "--"]);
assert!(res.is_err());
assert_eq!(res.unwrap_err().kind(), ErrorKind::MissingRequiredArgument);
}