Skip to content

Commit

Permalink
Merge pull request #5056 from ModProg/dynamic-completions-help
Browse files Browse the repository at this point in the history
Show arg/command descriptions in dynamic completions
  • Loading branch information
epage authored Aug 7, 2023
2 parents ca855c6 + aa3f47c commit fe04b3c
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 132 deletions.
1 change: 1 addition & 0 deletions clap_complete/examples/exhaustive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ fn cli() -> clap::Command {
clap::Command::new("cmd-brackets").about("List packages [filter]"),
clap::Command::new("cmd-expansions")
.about("Execute the shell command with $SHELL"),
clap::Command::new("escape-help").about("\\tab\t\"'\nNew Line"),
]),
clap::Command::new("value").args([
clap::Arg::new("delim").long("delim").value_delimiter(','),
Expand Down
99 changes: 77 additions & 22 deletions clap_complete/src/dynamic/completer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ffi::OsStr;
use std::ffi::OsString;

use clap::builder::StyledStr;
use clap_lex::OsStrExt as _;

/// Shell-specific completions
Expand Down Expand Up @@ -31,7 +32,7 @@ pub fn complete(
args: Vec<std::ffi::OsString>,
arg_index: usize,
current_dir: Option<&std::path::Path>,
) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
cmd.build();

let raw_args = clap_lex::RawArgs::new(args.into_iter());
Expand Down Expand Up @@ -90,7 +91,7 @@ fn complete_arg(
current_dir: Option<&std::path::Path>,
pos_index: usize,
is_escaped: bool,
) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
debug!(
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
arg,
Expand All @@ -109,26 +110,24 @@ 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)
}),
)
}
} else {
completions.extend(
crate::generator::utils::longs_and_visible_aliases(cmd)
.into_iter()
.filter_map(|f| f.starts_with(flag).then(|| format!("--{f}").into())),
);
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter_map(
|(f, help)| f.starts_with(flag).then(|| (format!("--{f}").into(), help)),
));
}
}
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
// HACK: Assuming knowledge of is_escape / is_stdio
completions.extend(
crate::generator::utils::longs_and_visible_aliases(cmd)
longs_and_visible_aliases(cmd)
.into_iter()
.map(|f| format!("--{f}").into()),
.map(|(f, help)| (format!("--{f}").into(), help)),
);
}

Expand All @@ -140,10 +139,10 @@ fn complete_arg(
};
// HACK: Assuming knowledge of is_stdio
completions.extend(
crate::generator::utils::shorts_and_visible_aliases(cmd)
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)),
);
}
}
Expand All @@ -166,15 +165,16 @@ fn complete_arg_value(
value: Result<&str, &OsStr>,
arg: &clap::Arg,
current_dir: Option<&std::path::Path>,
) -> Vec<OsString> {
) -> Vec<(OsString, Option<StyledStr>)> {
let mut values = Vec::new();
debug!("complete_arg_value: arg={arg:?}, value={value:?}");

if let Some(possible_values) = crate::generator::utils::possible_values(arg) {
if let Some(possible_values) = possible_values(arg) {
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 {
Expand Down Expand Up @@ -223,7 +223,7 @@ fn complete_path(
value_os: &OsStr,
current_dir: Option<&std::path::Path>,
is_wanted: impl Fn(&std::path::Path) -> bool,
) -> Vec<OsString> {
) -> Vec<(OsString, Option<StyledStr>)> {
let mut completions = Vec::new();

let current_dir = match current_dir {
Expand Down Expand Up @@ -255,32 +255,87 @@ 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));
}
}
}

completions
}

fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option<StyledStr>)> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
cmd.get_name(),
value
);

let mut scs = crate::generator::utils::subcommands(cmd)
let mut scs = subcommands(cmd)
.into_iter()
.filter(|x| x.0.starts_with(value))
.map(|x| OsString::from(&x.0))
.map(|x| (OsString::from(&x.0), x.1))
.collect::<Vec<_>>();
scs.sort();
scs.dedup();
scs
}

/// Gets all the long options, their visible aliases and flags of a [`clap::Command`].
/// Includes `help` and `version` depending on the [`clap::Command`] settings.
fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option<StyledStr>)> {
debug!("longs: name={}", p.get_name());

p.get_arguments()
.filter_map(|a| {
a.get_long_and_visible_aliases().map(|longs| {
longs
.into_iter()
.map(|s| (s.to_string(), a.get_help().cloned()))
})
})
.flatten()
.collect()
}

/// Gets all the short options, their visible aliases and flags of a [`clap::Command`].
/// Includes `h` and `V` depending on the [`clap::Command`] settings.
fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<(char, Option<StyledStr>)> {
debug!("shorts: name={}", p.get_name());

p.get_arguments()
.filter_map(|a| {
a.get_short_and_visible_aliases()
.map(|shorts| shorts.into_iter().map(|s| (s, a.get_help().cloned())))
})
.flatten()
.collect()
}

/// Get the possible values for completion
fn possible_values(a: &clap::Arg) -> Option<Vec<clap::builder::PossibleValue>> {
if !a.get_num_args().expect("built").takes_values() {
None
} else {
a.get_value_parser()
.possible_values()
.map(|pvs| pvs.collect())
}
}

/// Gets subcommands of [`clap::Command`] in the form of `("name", "bin_name")`.
///
/// Subcommand `rustup toolchain install` would be converted to
/// `("install", "rustup toolchain install")`.
fn subcommands(p: &clap::Command) -> Vec<(String, Option<StyledStr>)> {
debug!("subcommands: name={}", p.get_name());
debug!("subcommands: Has subcommands...{:?}", p.has_subcommands());

p.get_subcommands()
.map(|sc| (sc.get_name().to_string(), sc.get_about().cloned()))
.collect()
}
2 changes: 1 addition & 1 deletion clap_complete/src/dynamic/shells/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ complete -o nospace -o bashdefault -F _clap_complete_NAME BIN
let ifs: Option<String> = 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"))?;
}
Expand Down
12 changes: 10 additions & 2 deletions clap_complete/src/dynamic/shells/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ 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.to_string().lines().next().unwrap_or_default()
)?;
}
writeln!(buf)?;
}
Ok(())
}
Expand Down
57 changes: 54 additions & 3 deletions clap_complete/tests/snapshots/home/static/exhaustive/bash/.bashrc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ _exhaustive() {
exhaustive__help__quote,cmd-single-quotes)
cmd="exhaustive__help__quote__cmd__single__quotes"
;;
exhaustive__help__quote,escape-help)
cmd="exhaustive__help__quote__escape__help"
;;
exhaustive__pacman,help)
cmd="exhaustive__pacman__help"
;;
Expand Down Expand Up @@ -128,6 +131,9 @@ _exhaustive() {
exhaustive__quote,cmd-single-quotes)
cmd="exhaustive__quote__cmd__single__quotes"
;;
exhaustive__quote,escape-help)
cmd="exhaustive__quote__escape__help"
;;
exhaustive__quote,help)
cmd="exhaustive__quote__help"
;;
Expand All @@ -149,6 +155,9 @@ _exhaustive() {
exhaustive__quote__help,cmd-single-quotes)
cmd="exhaustive__quote__help__cmd__single__quotes"
;;
exhaustive__quote__help,escape-help)
cmd="exhaustive__quote__help__escape__help"
;;
exhaustive__quote__help,help)
cmd="exhaustive__quote__help__help"
;;
Expand Down Expand Up @@ -391,7 +400,7 @@ _exhaustive() {
return 0
;;
exhaustive__help__quote)
opts="cmd-single-quotes cmd-double-quotes cmd-backticks cmd-backslash cmd-brackets cmd-expansions"
opts="cmd-single-quotes cmd-double-quotes cmd-backticks cmd-backslash cmd-brackets cmd-expansions escape-help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down Expand Up @@ -488,6 +497,20 @@ _exhaustive() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
exhaustive__help__quote__escape__help)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
exhaustive__help__value)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
Expand Down Expand Up @@ -709,7 +732,7 @@ _exhaustive() {
return 0
;;
exhaustive__quote)
opts="-h -V --single-quotes --double-quotes --backticks --backslash --brackets --expansions --global --help --version cmd-single-quotes cmd-double-quotes cmd-backticks cmd-backslash cmd-brackets cmd-expansions help"
opts="-h -V --single-quotes --double-quotes --backticks --backslash --brackets --expansions --global --help --version cmd-single-quotes cmd-double-quotes cmd-backticks cmd-backslash cmd-brackets cmd-expansions escape-help help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down Expand Up @@ -806,8 +829,22 @@ _exhaustive() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
exhaustive__quote__escape__help)
opts="-h -V --global --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
exhaustive__quote__help)
opts="cmd-single-quotes cmd-double-quotes cmd-backticks cmd-backslash cmd-brackets cmd-expansions help"
opts="cmd-single-quotes cmd-double-quotes cmd-backticks cmd-backslash cmd-brackets cmd-expansions escape-help help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down Expand Up @@ -904,6 +941,20 @@ _exhaustive() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
exhaustive__quote__help__escape__help)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
exhaustive__quote__help__help)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
Expand Down
Loading

0 comments on commit fe04b3c

Please sign in to comment.