diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 617d73d269f..7a9015399a8 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -1,3 +1,4 @@ +use core::num; use std::ffi::OsStr; use std::ffi::OsString; @@ -71,8 +72,7 @@ pub fn complete( } if is_escaped { - pos_index += 1; - state = ParseState::Pos(pos_index); + (state, pos_index) = parse_positional(current_cmd, pos_index, is_escaped, state); } else if arg.is_escape() { is_escaped = true; state = ParseState::ValueDone; @@ -92,7 +92,7 @@ pub fn complete( if value.is_some() { ParseState::ValueDone } else { - ParseState::Opt(opt.unwrap()) + ParseState::Opt((opt.unwrap(), 1)) } } Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => { @@ -115,7 +115,7 @@ pub fn complete( Some(opt) => { state = match short.next_value_os() { Some(_) => ParseState::ValueDone, - None => ParseState::Opt(opt), + None => ParseState::Opt((opt, 1)), }; } None => { @@ -124,13 +124,24 @@ pub fn complete( } } else { match state { - ParseState::ValueDone | ParseState::Pos(_) => { - pos_index += 1; - state = ParseState::ValueDone; - } - ParseState::Opt(_) => { - state = ParseState::ValueDone; + ParseState::ValueDone | ParseState::Pos(..) => { + (state, pos_index) = + parse_positional(current_cmd, pos_index, is_escaped, state); } + + ParseState::Opt((ref opt, count)) => match opt.get_num_args() { + Some(range) => { + let max = range.max_values(); + if count < max { + state = ParseState::Opt((opt.clone(), count + 1)); + } else { + state = ParseState::ValueDone; + } + } + None => { + state = ParseState::ValueDone; + } + }, } } } @@ -146,11 +157,11 @@ enum ParseState<'a> { /// Parsing a value done, there is no state to record. ValueDone, - /// Parsing a positional argument after `--` - Pos(usize), + /// Parsing a positional argument after `--`. Pos(pos_index, takes_num_args) + Pos((usize, usize)), /// Parsing a optional flag argument - Opt(&'a clap::Arg), + Opt((&'a clap::Arg, usize)), } fn complete_arg( @@ -290,7 +301,7 @@ fn complete_arg( completions.extend(complete_subcommand(value, cmd)); } } - ParseState::Pos(_) => { + ParseState::Pos(..) => { if let Some(positional) = cmd .get_positionals() .find(|p| p.get_index() == Some(pos_index)) @@ -298,8 +309,19 @@ fn complete_arg( completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); } } - ParseState::Opt(opt) => { + ParseState::Opt((opt, count)) => { completions.extend(complete_arg_value(arg.to_value(), opt, current_dir)); + let min = opt.get_num_args().map(|r| r.min_values()).unwrap_or(0); + if count > min { + // Also complete this raw_arg as a positional argument, flags, options and subcommand. + completions.extend(complete_arg( + arg, + cmd, + current_dir, + pos_index, + ParseState::ValueDone, + )?); + } } } if completions.iter().any(|a| a.is_visible()) { @@ -582,6 +604,57 @@ fn parse_shortflags<'c, 's>( (leading_flags, takes_value_opt, short) } +/// Parse the positional arguments. Return the new state and the new positional index. +fn parse_positional<'a>( + cmd: &clap::Command, + pos_index: usize, + is_escaped: bool, + state: ParseState<'a>, +) -> (ParseState<'a>, usize) { + let pos_arg = cmd + .get_positionals() + .find(|p| p.get_index() == Some(pos_index)); + let num_args = pos_arg + .and_then(|a| a.get_num_args().and_then(|r| Some(r.max_values()))) + .unwrap_or(1); + + let update_state_with_new_positional = |pos_index| -> (ParseState<'a>, usize) { + if num_args > 1 { + (ParseState::Pos((pos_index, 1)), pos_index) + } else { + if is_escaped { + (ParseState::Pos((pos_index, 1)), pos_index + 1) + } else { + (ParseState::ValueDone, pos_index + 1) + } + } + }; + match state { + ParseState::ValueDone => { + update_state_with_new_positional(pos_index) + }, + ParseState::Pos((prev_pos_index, num_arg)) => { + if prev_pos_index == pos_index { + if num_arg + 1 < num_args { + (ParseState::Pos((pos_index, num_arg + 1)), pos_index) + } else { + if is_escaped { + (ParseState::Pos((pos_index, 1)), pos_index + 1) + } else { + (ParseState::ValueDone, pos_index + 1) + } + } + } else { + update_state_with_new_positional(pos_index) + } + } + ParseState::Opt(..) => unreachable!( + "This branch won't be hit, + because ParseState::Opt should not be seen as a positional argument and passed to this function." + ), + } +} + /// A completion candidate definition /// /// This makes it easier to add more fields to completion candidate, diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 0264aea38ab..0c0fe22b720 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -439,6 +439,252 @@ pos_c" ); } +#[test] +fn suggest_argument_multi_values() { + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("certain-num") + .long("certain-num") + .short('Y') + .value_parser(["val1", "val2", "val3"]) + .num_args(3), + ) + .arg( + clap::Arg::new("uncertain-num") + .long("uncertain-num") + .short('N') + .value_parser(["val1", "val2", "val3"]) + .num_args(1..=3), + ); + + assert_data_eq!( + complete!(cmd, "--certain-num [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "--certain-num val1 [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "--certain-num val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--uncertain-num [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "--uncertain-num val1 [TAB]"), + snapbox::str![ + "val1 +val2 +val3 +--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--uncertain-num val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-Y [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "-Y val1 [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "-Y val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-N [TAB]"), + snapbox::str![ + "val1 +val2 +val3" + ] + ); + + assert_data_eq!( + complete!(cmd, "-N val1 [TAB]"), + snapbox::str![ + "val1 +val2 +val3 +--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "-N val1 val2 val3 [TAB]"), + snapbox::str![ + "--certain-num +--uncertain-num +--help\tPrint help +-Y +-N +-h\tPrint help" + ] + ); +} + +#[test] +fn suggest_multi_positional() { + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("positional") + .value_parser(["pos_1, pos_2, pos_3"]) + .index(1), + ) + .arg( + clap::Arg::new("positional-2") + .value_parser(["pos_a", "pos_b", "pos_c"]) + .index(2) + .num_args(3), + ) + .arg( + clap::Arg::new("--format") + .long("format") + .short('F') + .value_parser(["json", "yaml", "toml"]), + ); + + assert_data_eq!( + complete!(cmd, "pos_1 pos_a [TAB]"), + snapbox::str![ + "pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "pos_1 pos_a pos_b [TAB]"), + snapbox::str![ + "pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json pos_1 [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help +pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json pos_1 pos_a [TAB]"), + snapbox::str![ + "pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json pos_1 pos_a pos_b pos_c [TAB]"), + snapbox::str![ + "--format +--help\tPrint help +-F +-h\tPrint help" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json -- pos_1 pos_a [TAB]"), + snapbox::str![ + "pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json -- pos_1 pos_a pos_b [TAB]"), + snapbox::str![ + "pos_a +pos_b +pos_c" + ] + ); + + assert_data_eq!( + complete!(cmd, "--format json -- pos_1 pos_a pos_b pos_c [TAB]"), + snapbox::str![""] + ); +} + 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())];