diff --git a/clap_builder/src/builder/arg.rs b/clap_builder/src/builder/arg.rs index 88baf9b22a3..e8ed39555d0 100644 --- a/clap_builder/src/builder/arg.rs +++ b/clap_builder/src/builder/arg.rs @@ -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, @@ -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) diff --git a/clap_builder/src/builder/arg_settings.rs b/clap_builder/src/builder/arg_settings.rs index fd47504047f..7a011e07473 100644 --- a/clap_builder/src/builder/arg_settings.rs +++ b/clap_builder/src/builder/arg_settings.rs @@ -49,6 +49,7 @@ pub(crate) enum ArgSettings { HidePossibleValues, AllowHyphenValues, AllowNegativeNumbers, + ForbidDashDashAsValue, RequireEquals, Last, TrailingVarArg, diff --git a/clap_builder/src/parser/parser.rs b/clap_builder/src/parser/parser.rs index 58269974dc1..ab50a88f6c7 100644 --- a/clap_builder/src/parser/parser.rs +++ b/clap_builder/src/parser/parser.rs @@ -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(); diff --git a/clap_complete/src/engine/complete.rs b/clap_complete/src/engine/complete.rs index a5bdfed4488..5147e07bbce 100644 --- a/clap_complete/src/engine/complete.rs +++ b/clap_complete/src/engine/complete.rs @@ -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(¤t_state, &arg) { match current_state { diff --git a/clap_complete/tests/testsuite/common.rs b/clap_complete/tests/testsuite/common.rs index 192356157b7..2c44c59e3b4 100644 --- a/clap_complete/tests/testsuite/common.rs +++ b/clap_complete/tests/testsuite/common.rs @@ -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( diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs index b8f03ee07e2..a003ffe1112 100644 --- a/clap_complete/tests/testsuite/engine.rs +++ b/clap_complete/tests/testsuite/engine.rs @@ -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") diff --git a/tests/builder/opts.rs b/tests/builder/opts.rs index 4459174fc18..dc121465137 100644 --- a/tests/builder/opts.rs +++ b/tests/builder/opts.rs @@ -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::("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::("first"), Some(&true)); + assert_eq!(matches.get_one::("second"), Some(&false)); + let remaining: Vec<_> = matches + .get_many::("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::("first"), Some(&false)); + assert_eq!(matches.get_one::("second"), Some(&true)); + let remaining: Vec<_> = matches + .get_many::("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::("first").map(|s| s.as_str()), None); + assert_eq!(matches.get_one::("second").map(|s| s.as_str()), Some("v2")); + let remaining: Vec<_> = matches + .get_many::("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") diff --git a/tests/builder/positionals.rs b/tests/builder/positionals.rs index 1a601d847cc..041b302b16d 100644 --- a/tests/builder/positionals.rs +++ b/tests/builder/positionals.rs @@ -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::("before") + .into_iter() + .flatten() + .map(|s| s.as_str()) + .collect(); + assert_eq!(before, vec!["--release"]); + + let after: Vec<_> = matches + .get_many::("after") + .into_iter() + .flatten() + .map(|s| s.as_str()) + .collect(); + assert_eq!(after, vec!["--expand-errors", "--rlimit=100"]); +} diff --git a/tests/derive/options.rs b/tests/derive/options.rs index 9ccd5a72579..04446a8d66e 100644 --- a/tests/derive/options.rs +++ b/tests/derive/options.rs @@ -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; @@ -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); +}