diff --git a/clap_complete/src/engine/complete.rs b/clap_complete/src/engine/complete.rs index b9bb252293e..75dad1b2b14 100644 --- a/clap_complete/src/engine/complete.rs +++ b/clap_complete/src/engine/complete.rs @@ -58,6 +58,11 @@ pub fn complete( parse_positional(current_cmd, pos_index, is_escaped, current_state); } else if arg.is_escape() { is_escaped = true; + } else if opt_allows_hyphen(¤t_state, &arg) { + match current_state { + ParseState::Opt((opt, count)) => next_state = parse_opt_value(opt, count), + _ => unreachable!("else branch is only reachable in Opt state"), + } } else if let Some((flag, value)) = arg.to_long() { if let Ok(flag) = flag { let opt = current_cmd.get_arguments().find(|a| { @@ -69,10 +74,14 @@ pub fn complete( }); is_find.unwrap_or(false) }); - if opt.map(|o| o.get_action().takes_values()).unwrap_or(false) { - if value.is_none() { - next_state = ParseState::Opt((opt.unwrap(), 1)); + + if let Some(opt) = opt { + if opt.get_action().takes_values() && value.is_none() { + next_state = ParseState::Opt((opt, 1)); }; + } else if pos_allows_hyphen(current_cmd, pos_index) { + (next_state, pos_index) = + parse_positional(current_cmd, pos_index, is_escaped, current_state); } } } else if let Some(short) = arg.to_short() { @@ -81,6 +90,9 @@ pub fn complete( if short.next_value_os().is_none() { next_state = ParseState::Opt((opt, 1)); } + } else if pos_allows_hyphen(current_cmd, pos_index) { + (next_state, pos_index) = + parse_positional(current_cmd, pos_index, is_escaped, current_state); } } else { match current_state { @@ -88,14 +100,7 @@ pub fn complete( (next_state, pos_index) = parse_positional(current_cmd, pos_index, is_escaped, current_state); } - - ParseState::Opt((opt, count)) => { - let range = opt.get_num_args().expect("built"); - let max = range.max_values(); - if count < max { - next_state = ParseState::Opt((opt, count + 1)); - } - } + ParseState::Opt((opt, count)) => next_state = parse_opt_value(opt, count), } } } @@ -546,3 +551,32 @@ fn parse_positional<'a>( ), } } + +/// Parse optional flag argument. Return new state +fn parse_opt_value(opt: &clap::Arg, count: usize) -> ParseState<'_> { + let range = opt.get_num_args().expect("built"); + let max = range.max_values(); + if count < max { + ParseState::Opt((opt, count + 1)) + } else { + ParseState::ValueDone + } +} + +fn pos_allows_hyphen(cmd: &clap::Command, pos_index: usize) -> bool { + cmd.get_positionals() + .find(|a| a.get_index() == Some(pos_index)) + .map(|p| p.is_allow_hyphen_values_set()) + .unwrap_or(false) +} + +fn opt_allows_hyphen(state: &ParseState<'_>, arg: &clap_lex::ParsedArg<'_>) -> bool { + let val = arg.to_value_os(); + if val.starts_with("-") { + if let ParseState::Opt((opt, _)) = state { + return opt.is_allow_hyphen_values_set(); + } + } + + false +} diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs index 5acb77bc9e2..d802bb148cf 100644 --- a/clap_complete/tests/testsuite/engine.rs +++ b/clap_complete/tests/testsuite/engine.rs @@ -981,6 +981,167 @@ a_pos,c_pos" ); } +#[test] +fn suggest_allow_hyphen() { + let mut cmd = Command::new("exhaustive") + .arg( + clap::Arg::new("format") + .long("format") + .short('F') + .allow_hyphen_values(true) + .value_parser(["--json", "--toml", "--yaml"]), + ) + .arg(clap::Arg::new("json").long("json")); + + assert_data_eq!(complete!(cmd, "--format --j[TAB]"), snapbox::str!["--json"]); + assert_data_eq!(complete!(cmd, "-F --j[TAB]"), snapbox::str!["--json"]); + assert_data_eq!(complete!(cmd, "--format --t[TAB]"), snapbox::str!["--toml"]); + assert_data_eq!(complete!(cmd, "-F --t[TAB]"), snapbox::str!["--toml"]); + + assert_data_eq!( + complete!(cmd, "--format --[TAB]"), + snapbox::str![ + "--json +--toml +--yaml" + ] + ); + + assert_data_eq!( + complete!(cmd, "-F --[TAB]"), + snapbox::str![ + "--json +--toml +--yaml" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format --json --j[TAB]"), + snapbox::str!["--json"] + ); + + assert_data_eq!( + complete!(cmd, "-F --json --j[TAB]"), + snapbox::str!["--json"] + ); +} + +#[test] +fn suggest_positional_long_allow_hyphen() { + let mut cmd = Command::new("exhaustive") + .arg( + clap::Arg::new("format") + .long("format") + .short('F') + .allow_hyphen_values(true) + .value_parser(["--json", "--toml", "--yaml"]), + ) + .arg( + clap::Arg::new("positional_a") + .value_parser(["--pos_a"]) + .index(1) + .allow_hyphen_values(true), + ) + .arg( + clap::Arg::new("positional_b") + .index(2) + .value_parser(["pos_b"]), + ); + + assert_data_eq!( + complete!(cmd, "--format --json --pos[TAB]"), + snapbox::str!["--pos_a"] + ); + assert_data_eq!( + complete!(cmd, "-F --json --pos[TAB]"), + snapbox::str!["--pos_a"] + ); + + assert_data_eq!( + complete!(cmd, "--format --json --pos_a [TAB]"), + snapbox::str![ + "--format +--help Print help +-F +-h Print help +pos_b" + ] + ); + assert_data_eq!( + complete!(cmd, "-F --json --pos_a [TAB]"), + snapbox::str![ + "--format +--help Print help +-F +-h Print help +pos_b" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format --json --pos_a p[TAB]"), + snapbox::str!["pos_b"] + ); + assert_data_eq!( + complete!(cmd, "-F --json --pos_a p[TAB]"), + snapbox::str!["pos_b"] + ); +} + +#[test] +fn suggest_positional_short_allow_hyphen() { + let mut cmd = Command::new("exhaustive") + .arg( + clap::Arg::new("format") + .long("format") + .short('F') + .allow_hyphen_values(true) + .value_parser(["--json", "--toml", "--yaml"]), + ) + .arg( + clap::Arg::new("positional_a") + .value_parser(["-a"]) + .index(1) + .allow_hyphen_values(true), + ) + .arg( + clap::Arg::new("positional_b") + .index(2) + .value_parser(["pos_b"]), + ); + + assert_data_eq!( + complete!(cmd, "--format --json -a [TAB]"), + snapbox::str![ + "--format +--help Print help +-F +-h Print help +pos_b" + ] + ); + assert_data_eq!( + complete!(cmd, "-F --json -a [TAB]"), + snapbox::str![ + "--format +--help Print help +-F +-h Print help +pos_b" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format --json -a p[TAB]"), + snapbox::str!["pos_b"] + ); + assert_data_eq!( + complete!(cmd, "-F --json -a p[TAB]"), + snapbox::str!["pos_b"] + ); +} + fn complete(cmd: &mut Command, args: impl AsRef, current_dir: Option<&Path>) -> String { let input = args.as_ref(); let mut args = vec![std::ffi::OsString::from(cmd.get_name())];