From 49b61168507f5cb27d290fe65d2b5ec2d3b79e45 Mon Sep 17 00:00:00 2001 From: Roland Fredenhagen Date: Sun, 30 Jul 2023 21:58:00 +0700 Subject: [PATCH] feat(complete): Show help in dynamic completions --- clap_complete/src/dynamic/completer.rs | 32 ++-- clap_complete/src/dynamic/shells/bash.rs | 2 +- clap_complete/src/dynamic/shells/fish.rs | 8 +- clap_complete/src/generator/utils.rs | 143 +++++++++--------- clap_complete/src/shells/bash.rs | 6 +- clap_complete/src/shells/zsh.rs | 4 +- clap_complete/tests/snapshots/aliases.bash | 2 +- .../tests/snapshots/feature_sample.bash | 2 +- .../home/static/exhaustive/bash/.bashrc | 2 +- .../tests/snapshots/special_commands.bash | 2 +- .../tests/snapshots/sub_subcommands.bash | 2 +- clap_complete/tests/testsuite/dynamic.rs | 8 +- clap_complete/tests/testsuite/fish.rs | 8 +- 13 files changed, 116 insertions(+), 105 deletions(-) diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs index 327e557e14cc..d550ef81812c 100644 --- a/clap_complete/src/dynamic/completer.rs +++ b/clap_complete/src/dynamic/completer.rs @@ -1,6 +1,7 @@ use std::ffi::OsStr; use std::ffi::OsString; +use clap::builder::StyledStr; use clap_lex::OsStrExt as _; /// Shell-specific completions @@ -31,7 +32,7 @@ pub fn complete( args: Vec, arg_index: usize, current_dir: Option<&std::path::Path>, -) -> Result, std::io::Error> { +) -> Result)>, std::io::Error> { cmd.build(); let raw_args = clap_lex::RawArgs::new(args.into_iter()); @@ -90,7 +91,7 @@ fn complete_arg( current_dir: Option<&std::path::Path>, pos_index: usize, is_escaped: bool, -) -> Result, std::io::Error> { +) -> Result)>, std::io::Error> { debug!( "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}", arg, @@ -109,9 +110,9 @@ fn complete_arg( completions.extend( complete_arg_value(value.to_str().ok_or(value), arg, current_dir) .into_iter() - .map(|os| { + .map(|(os, help)| { // HACK: Need better `OsStr` manipulation - format!("--{}={}", flag, os.to_string_lossy()).into() + (format!("--{}={}", flag, os.to_string_lossy()).into(), help) }), ) } @@ -119,7 +120,9 @@ fn complete_arg( completions.extend( crate::generator::utils::longs_and_visible_aliases(cmd) .into_iter() - .filter_map(|f| f.starts_with(flag).then(|| format!("--{f}").into())), + .filter_map(|(f, help)| { + f.starts_with(flag).then(|| (format!("--{f}").into(), help)) + }), ); } } @@ -128,7 +131,7 @@ fn complete_arg( completions.extend( crate::generator::utils::longs_and_visible_aliases(cmd) .into_iter() - .map(|f| format!("--{f}").into()), + .map(|(f, help)| (format!("--{f}").into(), help)), ); } @@ -143,7 +146,7 @@ fn complete_arg( crate::generator::utils::shorts_and_visible_aliases(cmd) .into_iter() // HACK: Need better `OsStr` manipulation - .map(|f| format!("{}{}", dash_or_arg, f).into()), + .map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)), ); } } @@ -166,7 +169,7 @@ fn complete_arg_value( value: Result<&str, &OsStr>, arg: &clap::Arg, current_dir: Option<&std::path::Path>, -) -> Vec { +) -> Vec<(OsString, Option)> { let mut values = Vec::new(); debug!("complete_arg_value: arg={arg:?}, value={value:?}"); @@ -174,7 +177,8 @@ fn complete_arg_value( if let Ok(value) = value { values.extend(possible_values.into_iter().filter_map(|p| { let name = p.get_name(); - name.starts_with(value).then(|| name.into()) + name.starts_with(value) + .then(|| (name.into(), p.get_help().cloned())) })); } } else { @@ -223,7 +227,7 @@ fn complete_path( value_os: &OsStr, current_dir: Option<&std::path::Path>, is_wanted: impl Fn(&std::path::Path) -> bool, -) -> Vec { +) -> Vec<(OsString, Option)> { let mut completions = Vec::new(); let current_dir = match current_dir { @@ -255,12 +259,12 @@ fn complete_path( let path = entry.path(); let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); suggestion.push(""); // Ensure trailing `/` - completions.push(suggestion.as_os_str().to_owned()); + completions.push((suggestion.as_os_str().to_owned(), None)); } else { let path = entry.path(); if is_wanted(&path) { let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); - completions.push(suggestion.as_os_str().to_owned()); + completions.push((suggestion.as_os_str().to_owned(), None)); } } } @@ -268,7 +272,7 @@ fn complete_path( completions } -fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec { +fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option)> { debug!( "complete_subcommand: cmd={:?}, value={:?}", cmd.get_name(), @@ -278,7 +282,7 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec { let mut scs = crate::generator::utils::subcommands(cmd) .into_iter() .filter(|x| x.0.starts_with(value)) - .map(|x| OsString::from(&x.0)) + .map(|x| (OsString::from(&x.0), x.2)) .collect::>(); scs.sort(); scs.dedup(); diff --git a/clap_complete/src/dynamic/shells/bash.rs b/clap_complete/src/dynamic/shells/bash.rs index ebc43c6910fe..43c128e5b4ec 100644 --- a/clap_complete/src/dynamic/shells/bash.rs +++ b/clap_complete/src/dynamic/shells/bash.rs @@ -73,7 +73,7 @@ complete -o nospace -o bashdefault -F _clap_complete_NAME BIN let ifs: Option = std::env::var("IFS").ok().and_then(|i| i.parse().ok()); let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - for (i, completion) in completions.iter().enumerate() { + for (i, (completion, _)) in completions.iter().enumerate() { if i != 0 { write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; } diff --git a/clap_complete/src/dynamic/shells/fish.rs b/clap_complete/src/dynamic/shells/fish.rs index 79b8bda543d4..a73259f01257 100644 --- a/clap_complete/src/dynamic/shells/fish.rs +++ b/clap_complete/src/dynamic/shells/fish.rs @@ -30,8 +30,12 @@ impl crate::dynamic::Completer for Fish { let index = args.len() - 1; let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; - for completion in completions { - writeln!(buf, "{}", completion.to_string_lossy())?; + for (completion, help) in completions { + write!(buf, "{}", completion.to_string_lossy())?; + if let Some(help) = help { + write!(buf, "\t{help}")?; + } + writeln!(buf)?; } Ok(()) } diff --git a/clap_complete/src/generator/utils.rs b/clap_complete/src/generator/utils.rs index ca76d189baa1..60eec46391c5 100644 --- a/clap_complete/src/generator/utils.rs +++ b/clap_complete/src/generator/utils.rs @@ -1,12 +1,12 @@ //! Helpers for writing generators -use clap::{Arg, Command}; +use clap::{builder::StyledStr, Arg, Command}; /// Gets all subcommands including child subcommands in the form of `("name", "bin_name")`. /// /// Subcommand `rustup toolchain install` would be converted to /// `("install", "rustup toolchain install")`. -pub fn all_subcommands(cmd: &Command) -> Vec<(String, String)> { +pub fn all_subcommands(cmd: &Command) -> Vec<(String, String, Option)> { let mut subcmds: Vec<_> = subcommands(cmd); for sc_v in cmd.get_subcommands().map(all_subcommands) { @@ -33,7 +33,7 @@ pub fn find_subcommand_with_path<'cmd>(p: &'cmd Command, path: Vec<&str>) -> &'c /// /// Subcommand `rustup toolchain install` would be converted to /// `("install", "rustup toolchain install")`. -pub fn subcommands(p: &Command) -> Vec<(String, String)> { +pub fn subcommands(p: &Command) -> Vec<(String, String, Option)> { debug!("subcommands: name={}", p.get_name()); debug!("subcommands: Has subcommands...{:?}", p.has_subcommands()); @@ -48,7 +48,11 @@ pub fn subcommands(p: &Command) -> Vec<(String, String)> { sc_bin_name ); - subcmds.push((sc.get_name().to_string(), sc_bin_name.to_string())); + subcmds.push(( + sc.get_name().to_string(), + sc_bin_name.to_string(), + sc.get_about().cloned(), + )); } subcmds @@ -56,24 +60,13 @@ pub fn subcommands(p: &Command) -> Vec<(String, String)> { /// Gets all the short options, their visible aliases and flags of a [`clap::Command`]. /// Includes `h` and `V` depending on the [`clap::Command`] settings. -pub fn shorts_and_visible_aliases(p: &Command) -> Vec { +pub fn shorts_and_visible_aliases(p: &Command) -> Vec<(char, Option)> { debug!("shorts: name={}", p.get_name()); p.get_arguments() .filter_map(|a| { - if !a.is_positional() { - if a.get_visible_short_aliases().is_some() && a.get_short().is_some() { - let mut shorts_and_visible_aliases = a.get_visible_short_aliases().unwrap(); - shorts_and_visible_aliases.push(a.get_short().unwrap()); - Some(shorts_and_visible_aliases) - } else if a.get_visible_short_aliases().is_none() && a.get_short().is_some() { - Some(vec![a.get_short().unwrap()]) - } else { - None - } - } else { - None - } + a.get_short_and_visible_aliases() + .map(|shorts| shorts.into_iter().map(|s| (s, a.get_help().cloned()))) }) .flatten() .collect() @@ -81,29 +74,16 @@ pub fn shorts_and_visible_aliases(p: &Command) -> Vec { /// Gets all the long options, their visible aliases and flags of a [`clap::Command`]. /// Includes `help` and `version` depending on the [`clap::Command`] settings. -pub fn longs_and_visible_aliases(p: &Command) -> Vec { +pub fn longs_and_visible_aliases(p: &Command) -> Vec<(String, Option)> { debug!("longs: name={}", p.get_name()); p.get_arguments() .filter_map(|a| { - if !a.is_positional() { - if a.get_visible_aliases().is_some() && a.get_long().is_some() { - let mut visible_aliases: Vec<_> = a - .get_visible_aliases() - .unwrap() - .into_iter() - .map(|s| s.to_string()) - .collect(); - visible_aliases.push(a.get_long().unwrap().to_string()); - Some(visible_aliases) - } else if a.get_visible_aliases().is_none() && a.get_long().is_some() { - Some(vec![a.get_long().unwrap().to_string()]) - } else { - None - } - } else { - None - } + a.get_long_and_visible_aliases().map(|longs| { + longs + .into_iter() + .map(|s| (s.to_string(), a.get_help().cloned())) + }) }) .flatten() .collect() @@ -136,6 +116,19 @@ mod tests { use clap::Arg; use clap::ArgAction; + const HELP: &str = "Print this message or the help of the given subcommand(s)"; + + macro_rules! assert_option { + ($option:expr, $value:expr) => { + assert_eq!($option.0, $value); + assert!($option.1.is_none()); + }; + ($option:expr, $value:expr, $help:expr) => { + assert_eq!($option.0, $value); + assert_eq!($option.1.as_ref().unwrap().to_string(), $help); + }; + } + fn common_app() -> Command { Command::new("myapp") .subcommand( @@ -171,36 +164,44 @@ mod tests { fn test_subcommands() { let cmd = built_with_version(); - assert_eq!( - subcommands(&cmd), - vec![ - ("test".to_string(), "my-cmd test".to_string()), - ("hello".to_string(), "my-cmd hello".to_string()), - ("help".to_string(), "my-cmd help".to_string()), - ] - ); + for (actual, expected) in subcommands(&cmd).into_iter().zip([ + ("test", "my-cmd test", None), + ("hello", "my-cmd hello", None), + ("help", "my-cmd help", Some(HELP)), + ]) { + assert_eq!(actual.0, expected.0); + assert_eq!(actual.1, expected.1); + assert_eq!( + actual.2.as_ref().map(ToString::to_string).as_deref(), + expected.2 + ); + } } #[test] fn test_all_subcommands() { let cmd = built_with_version(); - assert_eq!( - all_subcommands(&cmd), - vec![ - ("test".to_string(), "my-cmd test".to_string()), - ("hello".to_string(), "my-cmd hello".to_string()), - ("help".to_string(), "my-cmd help".to_string()), - ("config".to_string(), "my-cmd test config".to_string()), - ("help".to_string(), "my-cmd test help".to_string()), - ("config".to_string(), "my-cmd test help config".to_string()), - ("help".to_string(), "my-cmd test help help".to_string()), - ("test".to_string(), "my-cmd help test".to_string()), - ("hello".to_string(), "my-cmd help hello".to_string()), - ("help".to_string(), "my-cmd help help".to_string()), - ("config".to_string(), "my-cmd help test config".to_string()), - ] - ); + for (actual, expected) in subcommands(&cmd).into_iter().zip([ + ("test", "my-cmd test", None), + ("hello", "my-cmd hello", None), + ("help", "my-cmd help", Some(HELP)), + ("config", "my-cmd test config", None), + ("help", "my-cmd test help", Some(HELP)), + ("config", "my-cmd test help config", None), + ("help", "my-cmd test help help", Some(HELP)), + ("test", "my-cmd help test", None), + ("hello", "my-cmd help hello", None), + ("help", "my-cmd help help", Some(HELP)), + ("config", "my-cmd help test config", None), + ]) { + assert_eq!(actual.0, expected.0); + assert_eq!(actual.1, expected.1); + assert_eq!( + actual.2.as_ref().map(ToString::to_string).as_deref(), + expected.2 + ); + } } #[test] @@ -248,15 +249,15 @@ mod tests { let shorts = shorts_and_visible_aliases(&cmd); assert_eq!(shorts.len(), 2); - assert_eq!(shorts[0], 'h'); - assert_eq!(shorts[1], 'V'); + assert_option!(shorts[0], 'h', "Print help"); + assert_option!(shorts[1], 'V', "Print version"); let sc_shorts = shorts_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"])); assert_eq!(sc_shorts.len(), 3); - assert_eq!(sc_shorts[0], 'p'); - assert_eq!(sc_shorts[1], 'f'); - assert_eq!(sc_shorts[2], 'h'); + assert_option!(sc_shorts[0], 'f'); + assert_option!(sc_shorts[1], 'p'); + assert_option!(sc_shorts[2], 'h', "Print help"); } #[test] @@ -265,14 +266,14 @@ mod tests { let longs = longs_and_visible_aliases(&cmd); assert_eq!(longs.len(), 2); - assert_eq!(longs[0], "help"); - assert_eq!(longs[1], "version"); + assert_option!(longs[0], "help", "Print help"); + assert_option!(longs[1], "version", "Print version"); let sc_longs = longs_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"])); assert_eq!(sc_longs.len(), 3); - assert_eq!(sc_longs[0], "path"); - assert_eq!(sc_longs[1], "file"); - assert_eq!(sc_longs[2], "help"); + assert_option!(sc_longs[0], "file"); + assert_option!(sc_longs[1], "path"); + assert_option!(sc_longs[2], "help", "Print help"); } } diff --git a/clap_complete/src/shells/bash.rs b/clap_complete/src/shells/bash.rs index 2a97e1de2fea..32dce88d35d5 100644 --- a/clap_complete/src/shells/bash.rs +++ b/clap_complete/src/shells/bash.rs @@ -219,10 +219,10 @@ fn all_options_for_path(cmd: &Command, path: &str) -> String { let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); let mut opts = String::new(); - for short in utils::shorts_and_visible_aliases(p) { + for (short, _) in utils::shorts_and_visible_aliases(p) { write!(&mut opts, "-{short} ").unwrap(); } - for long in utils::longs_and_visible_aliases(p) { + for (long, _) in utils::longs_and_visible_aliases(p) { write!(&mut opts, "--{long} ").unwrap(); } for pos in p.get_positionals() { @@ -234,7 +234,7 @@ fn all_options_for_path(cmd: &Command, path: &str) -> String { write!(&mut opts, "{pos} ").unwrap(); } } - for (sc, _) in utils::subcommands(p) { + for (sc, ..) in utils::subcommands(p) { write!(&mut opts, "{sc} ").unwrap(); } opts.pop(); diff --git a/clap_complete/src/shells/zsh.rs b/clap_complete/src/shells/zsh.rs index 65d7af65b5f9..284336bd4753 100644 --- a/clap_complete/src/shells/zsh.rs +++ b/clap_complete/src/shells/zsh.rs @@ -115,7 +115,7 @@ _{bin_name_underscore}_commands() {{ all_subcommands.sort(); all_subcommands.dedup(); - for (_, ref bin_name) in &all_subcommands { + for (_, ref bin_name, _) in &all_subcommands { debug!("subcommand_details:iter: bin_name={bin_name}"); ret.push(format!( @@ -227,7 +227,7 @@ fn get_subcommands_of(parent: &Command) -> String { let subcommand_names = utils::subcommands(parent); let mut all_subcommands = vec![]; - for (ref name, ref bin_name) in &subcommand_names { + for (ref name, ref bin_name, _) in &subcommand_names { debug!( "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}", parent.get_name(), diff --git a/clap_complete/tests/snapshots/aliases.bash b/clap_complete/tests/snapshots/aliases.bash index 156515872422..7ae860c55872 100644 --- a/clap_complete/tests/snapshots/aliases.bash +++ b/clap_complete/tests/snapshots/aliases.bash @@ -19,7 +19,7 @@ _my-app() { case "${cmd}" in my__app) - opts="-F -f -O -o -h -V --flg --flag --opt --option --help --version [positional]" + opts="-f -F -o -O -h -V --flag --flg --option --opt --help --version [positional]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/clap_complete/tests/snapshots/feature_sample.bash b/clap_complete/tests/snapshots/feature_sample.bash index e5119ae61098..7d8ca32beed9 100644 --- a/clap_complete/tests/snapshots/feature_sample.bash +++ b/clap_complete/tests/snapshots/feature_sample.bash @@ -31,7 +31,7 @@ _my-app() { case "${cmd}" in my__app) - opts="-C -c -h -V --conf --config --help --version [file] first second test help" + opts="-c -C -h -V --config --conf --help --version [file] first second test help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc index 9ef12ac7edbc..36374ab4d163 100644 --- a/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc +++ b/clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc @@ -199,7 +199,7 @@ _exhaustive() { return 0 ;; exhaustive__alias) - opts="-F -f -O -o -h -V --flg --flag --opt --option --global --help --version [positional]" + opts="-f -F -o -O -h -V --flag --flg --option --opt --global --help --version [positional]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/clap_complete/tests/snapshots/special_commands.bash b/clap_complete/tests/snapshots/special_commands.bash index 12b3d1543481..466de8d4e398 100644 --- a/clap_complete/tests/snapshots/special_commands.bash +++ b/clap_complete/tests/snapshots/special_commands.bash @@ -49,7 +49,7 @@ _my-app() { case "${cmd}" in my__app) - opts="-C -c -h -V --conf --config --help --version [file] first second test some_cmd some-cmd-with-hyphens some-hidden-cmd help" + opts="-c -C -h -V --config --conf --help --version [file] first second test some_cmd some-cmd-with-hyphens some-hidden-cmd help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/clap_complete/tests/snapshots/sub_subcommands.bash b/clap_complete/tests/snapshots/sub_subcommands.bash index 0b64301de04a..b92b8ead7196 100644 --- a/clap_complete/tests/snapshots/sub_subcommands.bash +++ b/clap_complete/tests/snapshots/sub_subcommands.bash @@ -55,7 +55,7 @@ _my-app() { case "${cmd}" in my__app) - opts="-C -c -h -V --conf --config --help --version [file] first second test some_cmd help" + opts="-c -C -h -V --config --conf --help --version [file] first second test some_cmd help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs index 8ab7461d033d..8a2d9e466b43 100644 --- a/clap_complete/tests/testsuite/dynamic.rs +++ b/clap_complete/tests/testsuite/dynamic.rs @@ -19,7 +19,7 @@ fn suggest_subcommand_subset() { clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap(); let completions = completions .into_iter() - .map(|s| s.to_string_lossy().into_owned()) + .map(|s| s.0.to_string_lossy().into_owned()) .collect::>(); assert_eq!(completions, ["hello-moon", "hello-world", "help"]); @@ -56,7 +56,7 @@ fn suggest_long_flag_subset() { clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap(); let completions = completions .into_iter() - .map(|s| s.to_string_lossy().into_owned()) + .map(|s| s.0.to_string_lossy().into_owned()) .collect::>(); assert_eq!(completions, ["--hello-world", "--hello-moon", "--help"]); @@ -82,7 +82,7 @@ fn suggest_possible_value_subset() { clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap(); let completions = completions .into_iter() - .map(|s| s.to_string_lossy().into_owned()) + .map(|s| s.0.to_string_lossy().into_owned()) .collect::>(); assert_eq!(completions, ["hello-world", "hello-moon"]); @@ -119,7 +119,7 @@ fn suggest_additional_short_flags() { clap_complete::dynamic::complete(&mut cmd, args, arg_index, current_dir).unwrap(); let completions = completions .into_iter() - .map(|s| s.to_string_lossy().into_owned()) + .map(|s| s.0.to_string_lossy().into_owned()) .collect::>(); assert_eq!(completions, ["-aa", "-ab", "-ac", "-ah"]); diff --git a/clap_complete/tests/testsuite/fish.rs b/clap_complete/tests/testsuite/fish.rs index 6e2bbeb46fe4..be7ba89f6124 100644 --- a/clap_complete/tests/testsuite/fish.rs +++ b/clap_complete/tests/testsuite/fish.rs @@ -162,9 +162,11 @@ fn complete_dynamic() { let input = "exhaustive \t"; let expected = r#"% exhaustive -action help pacman -h --global -alias hint quote -V --help -complete last value --generate --version"#; +action last -V (Print version) +alias pacman --generate (generate) +complete (Register shell completions for this program) quote --global (everywhere) +help (Print this message or the help of the given subcommand(s)) value --help (Print help) +hint -h (Print help) --version (Print version)"#; let actual = runtime.complete(input, &term).unwrap(); snapbox::assert_eq(expected, actual); }