diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 19966f47665..5f540c3d16a 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -53,9 +53,10 @@ pub fn complete( let mut current_cmd = &*cmd; let mut pos_index = 1; let mut is_escaped = false; + let mut state = ParseState::ValueDone; while let Some(arg) = raw_args.next(&mut cursor) { if cursor == target_cursor { - return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped); + return complete_arg(&arg, current_cmd, current_dir, pos_index, state); } debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),); @@ -64,18 +65,103 @@ pub fn complete( if let Some(next_cmd) = current_cmd.find_subcommand(value) { current_cmd = next_cmd; pos_index = 1; + state = ParseState::ValueDone; continue; } } if is_escaped { pos_index += 1; + state = ParseState::Pos(pos_index); } else if arg.is_escape() { is_escaped = true; - } else if let Some(_long) = arg.to_long() { - } else if let Some(_short) = arg.to_short() { + state = ParseState::ValueDone; + } else if let Some((flag, value)) = arg.to_long() { + if let Ok(flag) = flag { + let opt = current_cmd.get_arguments().find(|a| { + let longs = a.get_long_and_visible_aliases(); + let is_find = longs.map(|v| { + let mut iter = v.into_iter(); + let s = iter.find(|s| *s == flag); + s.is_some() + }); + is_find.unwrap_or(false) + }); + state = match opt.map(|o| o.get_action()) { + Some(clap::ArgAction::Set) | Some(clap::ArgAction::Append) => { + if value.is_some() { + ParseState::ValueDone + } else { + ParseState::Opt(opt.unwrap().clone()) + } + } + Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => { + ParseState::ValueDone + } + Some(clap::ArgAction::Count) => ParseState::ValueDone, + Some(clap::ArgAction::Version) => ParseState::ValueDone, + Some(clap::ArgAction::Help) + | Some(clap::ArgAction::HelpLong) + | Some(clap::ArgAction::HelpShort) => ParseState::ValueDone, + Some(_) => ParseState::ValueDone, + None => ParseState::ValueDone, + }; + } else { + state = ParseState::ValueDone; + } + } else if let Some(mut short) = arg.to_short() { + let mut takes_value = false; + loop { + if let Some(Ok(opt)) = short.next_flag() { + let opt = current_cmd.get_arguments().find(|a| { + let shorts = a.get_short_and_visible_aliases(); + let is_find = shorts.map(|v| { + let mut iter = v.into_iter(); + let c = iter.find(|c| *c == opt); + c.is_some() + }); + is_find.unwrap_or(false) + }); + + state = match opt.map(|o| o.get_action()) { + Some(clap::ArgAction::Set) | Some(clap::ArgAction::Append) => { + takes_value = true; + if short.next_value_os().is_some() { + ParseState::ValueDone + } else { + ParseState::Opt(opt.unwrap().clone()) + } + } + Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => { + ParseState::ValueDone + } + Some(clap::ArgAction::Count) => ParseState::ValueDone, + Some(clap::ArgAction::Version) => ParseState::ValueDone, + Some(clap::ArgAction::Help) + | Some(clap::ArgAction::HelpShort) + | Some(clap::ArgAction::HelpLong) => ParseState::ValueDone, + Some(_) => ParseState::ValueDone, + None => ParseState::ValueDone, + }; + + if takes_value { + break; + } + } else { + state = ParseState::ValueDone; + break; + } + } } else { - pos_index += 1; + match state { + ParseState::ValueDone | ParseState::Pos(_) => { + pos_index += 1; + state = ParseState::ValueDone; + } + ParseState::Opt(_) => { + state = ParseState::ValueDone; + } + } } } @@ -85,96 +171,123 @@ pub fn complete( )) } +#[derive(Debug, PartialEq, Eq, Clone)] +enum ParseState { + /// Parsing a value done, there is no state to record. + ValueDone, + + /// Parsing a positional argument after `--` + Pos(usize), + + /// Parsing a optional flag argument + Opt(clap::Arg), +} + fn complete_arg( arg: &clap_lex::ParsedArg<'_>, cmd: &clap::Command, current_dir: Option<&std::path::Path>, pos_index: usize, - is_escaped: bool, + state: ParseState, ) -> Result, std::io::Error> { debug!( - "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}", + "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={:?}, state={:?}", arg, cmd.get_name(), current_dir, pos_index, - is_escaped + state ); let mut completions = Vec::::new(); - if !is_escaped { - if let Some((flag, value)) = arg.to_long() { - if let Ok(flag) = flag { - if let Some(value) = value { - if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) { - completions.extend( - complete_arg_value(value.to_str().ok_or(value), arg, current_dir) - .into_iter() - .map(|comp| { - CompletionCandidate::new(format!( - "--{}={}", - flag, - comp.get_content().to_string_lossy() - )) - .help(comp.get_help().cloned()) - .visible(comp.is_visible()) - }), - ); + match state { + ParseState::ValueDone => { + if let Some((flag, value)) = arg.to_long() { + if let Ok(flag) = flag { + if let Some(value) = value { + if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) + { + completions.extend( + complete_arg_value(value.to_str().ok_or(value), arg, current_dir) + .into_iter() + .map(|comp| { + CompletionCandidate::new(format!( + "--{}={}", + flag, + comp.get_content().to_string_lossy() + )) + .help(comp.get_help().cloned()) + .visible(comp.is_visible()) + }), + ); + } + } else { + completions.extend(longs_and_visible_aliases(cmd).into_iter().filter( + |comp| { + comp.get_content() + .starts_with(format!("--{}", flag).as_str()) + }, + )); + + completions.extend(hidden_longs_aliases(cmd).into_iter().filter(|comp| { + comp.get_content() + .starts_with(format!("--{}", flag).as_str()) + })) } - } else { - completions.extend(longs_and_visible_aliases(cmd).into_iter().filter(|comp| { - comp.get_content() - .starts_with(format!("--{}", flag).as_str()) - })); - - completions.extend(hidden_longs_aliases(cmd).into_iter().filter(|comp| { - comp.get_content() - .starts_with(format!("--{}", flag).as_str()) - })) } - } - } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() { - // HACK: Assuming knowledge of is_escape / is_stdio - completions.extend(longs_and_visible_aliases(cmd)); + } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() { + // HACK: Assuming knowledge of is_escape / is_stdio + completions.extend(longs_and_visible_aliases(cmd)); - completions.extend(hidden_longs_aliases(cmd)); - } + completions.extend(hidden_longs_aliases(cmd)); + } - if arg.is_empty() || arg.is_stdio() || arg.is_short() { - let dash_or_arg = if arg.is_empty() { - "-".into() - } else { - arg.to_value_os().to_string_lossy() - }; - // HACK: Assuming knowledge of is_stdio - completions.extend( - shorts_and_visible_aliases(cmd) - .into_iter() - // HACK: Need better `OsStr` manipulation - .map(|comp| { - CompletionCandidate::new(format!( - "{}{}", - dash_or_arg, - comp.get_content().to_string_lossy() - )) - .help(comp.get_help().cloned()) - .visible(true) - }), - ); - } - } + if arg.is_empty() || arg.is_stdio() || arg.is_short() { + let dash_or_arg = if arg.is_empty() { + "-".into() + } else { + arg.to_value_os().to_string_lossy() + }; + // HACK: Assuming knowledge of is_stdio + completions.extend( + shorts_and_visible_aliases(cmd) + .into_iter() + // HACK: Need better `OsStr` manipulation + .map(|comp| { + CompletionCandidate::new(format!( + "{}{}", + dash_or_arg, + comp.get_content().to_string_lossy() + )) + .help(comp.get_help().cloned()) + .visible(true) + }), + ); + } - if let Some(positional) = cmd - .get_positionals() - .find(|p| p.get_index() == Some(pos_index)) - { - completions.extend(complete_arg_value(arg.to_value(), positional, current_dir).into_iter()); - } + if let Some(positional) = cmd + .get_positionals() + .find(|p| p.get_index() == Some(pos_index)) + { + completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); + } - if let Ok(value) = arg.to_value() { - completions.extend(complete_subcommand(value, cmd)); + if let Ok(value) = arg.to_value() { + completions.extend(complete_subcommand(value, cmd)); + } + } + ParseState::Pos(_) => { + if let Some(positional) = cmd + .get_positionals() + .find(|p| p.get_index() == Some(pos_index)) + { + completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); + } + } + ParseState::Opt(opt) => { + completions.extend(complete_arg_value(arg.to_value(), &opt, current_dir)); + } } - if completions.iter().any(|a| a.is_visible()) { completions.retain(|a| a.is_visible()) } diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 6eff7f29c18..7087a8e09bf 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -1,5 +1,6 @@ #![cfg(feature = "unstable-dynamic")] +use std::fs; use std::path::Path; use clap::{builder::PossibleValue, Command}; @@ -9,7 +10,7 @@ macro_rules! complete { ($cmd:expr, $input:expr$(, current_dir = $current_dir:expr)? $(,)?) => { { #[allow(unused)] - let current_dir = None; + let current_dir: Option<&Path> = None; $(let current_dir = $current_dir;)? complete(&mut $cmd, $input, current_dir) } @@ -288,6 +289,121 @@ goodbye-world ); } +#[test] +fn suggest_argument_value() { + let mut cmd = Command::new("dynamic") + .arg( + clap::Arg::new("input") + .long("input") + .short('i') + .value_hint(clap::ValueHint::FilePath), + ) + .arg( + clap::Arg::new("format") + .long("format") + .short('F') + .value_parser(["json", "yaml", "toml"]), + ) + .arg( + clap::Arg::new("count") + .long("count") + .short('c') + .action(clap::ArgAction::Count), + ) + .arg(clap::Arg::new("positional").value_parser(["pos_a", "pos_b", "pos_c"])) + .args_conflicts_with_subcommands(true); + + let testdir = snapbox::dir::DirRoot::mutable_temp().unwrap(); + let testdir_path = testdir.path().unwrap(); + + fs::write(testdir_path.join("a_file"), "").unwrap(); + fs::write(testdir_path.join("b_file"), "").unwrap(); + fs::create_dir_all(testdir_path.join("c_dir")).unwrap(); + fs::create_dir_all(testdir_path.join("d_dir")).unwrap(); + + assert_data_eq!( + complete!(cmd, "--input [TAB]", current_dir = Some(testdir_path)), + snapbox::str![ + "a_file +b_file +c_dir/ +d_dir/" + ], + ); + + assert_data_eq!( + complete!(cmd, "-i [TAB]", current_dir = Some(testdir_path)), + snapbox::str![ + "a_file +b_file +c_dir/ +d_dir/" + ], + ); + + assert_data_eq!( + complete!(cmd, "--input a[TAB]", current_dir = Some(testdir_path)), + snapbox::str!["a_file"], + ); + + assert_data_eq!( + complete!(cmd, "-i b[TAB]", current_dir = Some(testdir_path)), + snapbox::str!["b_file"], + ); + + assert_data_eq!( + complete!(cmd, "--format [TAB]"), + snapbox::str![ + "json +yaml +toml" + ], + ); + + assert_data_eq!( + complete!(cmd, "-F [TAB]"), + snapbox::str![ + "json +yaml +toml" + ], + ); + + 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, "-cccF [TAB]"), + snapbox::str![ + "json +yaml +toml" + ] + ); + + assert_data_eq!( + complete!(cmd, "--input a_file [TAB]"), + snapbox::str![ + "--input +--format +--count +--help\tPrint help +-i +-F +-c +-h\tPrint help +pos_a +pos_b +pos_c" + ] + ); +} + 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())];