Skip to content

Commit

Permalink
feat(complete): Show help in dynamic completions
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Jul 31, 2023
1 parent 40650fa commit 49b6116
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 105 deletions.
32 changes: 18 additions & 14 deletions 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
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,17 +110,19 @@ 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())),
.filter_map(|(f, help)| {
f.starts_with(flag).then(|| (format!("--{f}").into(), help))
}),
);
}
}
Expand All @@ -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)),
);
}

Expand All @@ -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)),
);
}
}
Expand All @@ -166,15 +169,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 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 +227,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,20 +259,20 @@ 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(),
Expand All @@ -278,7 +282,7 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
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::<Vec<_>>();
scs.sort();
scs.dedup();
Expand Down
2 changes: 1 addition & 1 deletion clap_complete/src/dynamic/shells/bash.rs
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
8 changes: 6 additions & 2 deletions clap_complete/src/dynamic/shells/fish.rs
Expand Up @@ -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(())
}
Expand Down
143 changes: 72 additions & 71 deletions 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<StyledStr>)> {
let mut subcmds: Vec<_> = subcommands(cmd);

for sc_v in cmd.get_subcommands().map(all_subcommands) {
Expand All @@ -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<StyledStr>)> {
debug!("subcommands: name={}", p.get_name());
debug!("subcommands: Has subcommands...{:?}", p.has_subcommands());

Expand All @@ -48,62 +48,42 @@ 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
}

/// 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<char> {
pub fn shorts_and_visible_aliases(p: &Command) -> Vec<(char, Option<StyledStr>)> {
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()
}

/// 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<String> {
pub fn longs_and_visible_aliases(p: &Command) -> Vec<(String, Option<StyledStr>)> {
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()
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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");
}
}

0 comments on commit 49b6116

Please sign in to comment.