Skip to content

Commit

Permalink
Merge pull request #5583 from shannmu/hidden_flags_and_aliases
Browse files Browse the repository at this point in the history
feat(clap_complete): Support hiding long flags and their long aliases
  • Loading branch information
epage committed Jul 19, 2024
2 parents 7f90300 + d1e0f60 commit 8710057
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 52 deletions.
15 changes: 15 additions & 0 deletions clap_builder/src/builder/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3952,6 +3952,21 @@ impl Arg {
Some(longs)
}

/// Get hidden aliases for this argument, if any
#[inline]
pub fn get_aliases(&self) -> Option<Vec<&str>> {
if self.aliases.is_empty() {
None
} else {
Some(
self.aliases
.iter()
.filter_map(|(s, v)| if !*v { Some(s.as_str()) } else { None })
.collect(),
)
}
}

/// Get the names of possible values for this argument. Only useful for user
/// facing applications, such as building help messages or man files
pub fn get_possible_values(&self) -> Vec<PossibleValue> {
Expand Down
166 changes: 136 additions & 30 deletions clap_complete/src/dynamic/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn complete(
args: Vec<OsString>,
arg_index: usize,
current_dir: Option<&std::path::Path>,
) -> Result<Vec<(OsString, Option<StyledStr>)>, std::io::Error> {
) -> Result<Vec<CompletionCandidate>, std::io::Error> {
cmd.build();

let raw_args = clap_lex::RawArgs::new(args);
Expand Down Expand Up @@ -91,7 +91,7 @@ fn complete_arg(
current_dir: Option<&std::path::Path>,
pos_index: usize,
is_escaped: bool,
) -> Result<Vec<(OsString, Option<StyledStr>)>, std::io::Error> {
) -> Result<Vec<CompletionCandidate>, std::io::Error> {
debug!(
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
arg,
Expand All @@ -100,7 +100,7 @@ fn complete_arg(
pos_index,
is_escaped
);
let mut completions = Vec::new();
let mut completions = Vec::<CompletionCandidate>::new();

if !is_escaped {
if let Some((flag, value)) = arg.to_long() {
Expand All @@ -112,23 +112,33 @@ fn complete_arg(
.into_iter()
.map(|(os, help)| {
// HACK: Need better `OsStr` manipulation
(format!("--{}={}", flag, os.to_string_lossy()).into(), help)
CompletionCandidate::new(format!(
"--{}={}",
flag,
os.to_string_lossy()
))
.help(help)
.visible(true)
}),
);
}
} else {
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter_map(
|(f, help)| f.starts_with(flag).then(|| (format!("--{f}").into(), help)),
));
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)
.into_iter()
.map(|(f, help)| (format!("--{f}").into(), help)),
);
completions.extend(longs_and_visible_aliases(cmd));

completions.extend(hidden_longs_aliases(cmd));
}

if arg.is_empty() || arg.is_stdio() || arg.is_short() {
Expand All @@ -142,7 +152,15 @@ fn complete_arg(
shorts_and_visible_aliases(cmd)
.into_iter()
// HACK: Need better `OsStr` manipulation
.map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)),
.map(|comp| {
CompletionCandidate::new(format!(
"{}{}",
dash_or_arg,
comp.get_content().to_string_lossy()
))
.help(comp.get_help().cloned())
.visible(true)
}),
);
}
}
Expand All @@ -151,13 +169,21 @@ fn complete_arg(
.get_positionals()
.find(|p| p.get_index() == Some(pos_index))
{
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
completions.extend(
complete_arg_value(arg.to_value(), positional, current_dir)
.into_iter()
.map(|(os, help)| CompletionCandidate::new(os).help(help).visible(true)),
);
}

if let Ok(value) = arg.to_value() {
completions.extend(complete_subcommand(value, cmd));
}

if completions.iter().any(|a| a.is_visible()) {
completions.retain(|a| a.is_visible())
}

Ok(completions)
}

Expand All @@ -174,7 +200,7 @@ fn complete_arg_value(
values.extend(possible_values.into_iter().filter_map(|p| {
let name = p.get_name();
name.starts_with(value)
.then(|| (name.into(), p.get_help().cloned()))
.then(|| (OsString::from(name), p.get_help().cloned()))
}));
}
} else {
Expand Down Expand Up @@ -268,7 +294,7 @@ fn complete_path(
completions
}

fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option<StyledStr>)> {
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<CompletionCandidate> {
debug!(
"complete_subcommand: cmd={:?}, value={:?}",
cmd.get_name(),
Expand All @@ -277,25 +303,44 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Optio

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

/// Gets all the long options, their visible aliases and flags of a [`clap::Command`].
/// Gets all the long options, their visible aliases and flags of a [`clap::Command`] with formatted `--` prefix.
/// Includes `help` and `version` depending on the [`clap::Command`] settings.
fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option<StyledStr>)> {
fn longs_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
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()))
longs.into_iter().map(|s| {
CompletionCandidate::new(format!("--{}", s.to_string()))
.help(a.get_help().cloned())
.visible(!a.is_hide_set())
})
})
})
.flatten()
.collect()
}

/// Gets all the long hidden aliases and flags of a [`clap::Command`].
fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
debug!("longs: name={}", p.get_name());

p.get_arguments()
.filter_map(|a| {
a.get_aliases().map(|longs| {
longs.into_iter().map(|s| {
CompletionCandidate::new(format!("--{}", s.to_string()))
.help(a.get_help().cloned())
.visible(false)
})
})
})
.flatten()
Expand All @@ -304,13 +349,18 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option<StyledStr

/// 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>)> {
fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
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())))
a.get_short_and_visible_aliases().map(|shorts| {
shorts.into_iter().map(|s| {
CompletionCandidate::new(s.to_string())
.help(a.get_help().cloned())
.visible(!a.is_hide_set())
})
})
})
.flatten()
.collect()
Expand All @@ -331,14 +381,70 @@ fn possible_values(a: &clap::Arg) -> Option<Vec<clap::builder::PossibleValue>> {
///
/// Subcommand `rustup toolchain install` would be converted to
/// `("install", "rustup toolchain install")`.
fn subcommands(p: &clap::Command) -> Vec<(String, Option<StyledStr>)> {
fn subcommands(p: &clap::Command) -> Vec<CompletionCandidate> {
debug!("subcommands: name={}", p.get_name());
debug!("subcommands: Has subcommands...{:?}", p.has_subcommands());
p.get_subcommands()
.flat_map(|sc| {
sc.get_name_and_visible_aliases()
.into_iter()
.map(|s| (s.to_string(), sc.get_about().cloned()))
sc.get_name_and_visible_aliases().into_iter().map(|s| {
CompletionCandidate::new(s.to_string())
.help(sc.get_about().cloned())
.visible(true)
})
})
.collect()
}

/// A completion candidate definition
///
/// This makes it easier to add more fields to completion candidate,
/// rather than using `(OsString, Option<StyledStr>)` or `(String, Option<StyledStr>)` to represent a completion candidate
#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CompletionCandidate {
/// Main completion candidate content
content: OsString,

/// Help message with a completion candidate
help: Option<StyledStr>,

/// Whether the completion candidate is visible
visible: bool,
}

impl CompletionCandidate {
/// Create a new completion candidate
pub fn new(content: impl Into<OsString>) -> Self {
let content = content.into();
Self {
content,
..Default::default()
}
}

/// Set the help message of the completion candidate
pub fn help(mut self, help: Option<StyledStr>) -> Self {
self.help = help;
self
}

/// Set the visibility of the completion candidate
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}

/// Get the content of the completion candidate
pub fn get_content(&self) -> &OsStr {
&self.content
}

/// Get the help message of the completion candidate
pub fn get_help(&self) -> Option<&StyledStr> {
self.help.as_ref()
}

/// Get the visibility of the completion candidate
pub fn is_visible(&self) -> bool {
self.visible
}
}
4 changes: 2 additions & 2 deletions clap_complete/src/dynamic/shells/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ 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, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(buf, "{}", completion.to_string_lossy())?;
write!(buf, "{}", candidate.get_content().to_string_lossy())?;
}
Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions clap_complete/src/dynamic/shells/elvish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ set edit:completion:arg-completer[BIN] = { |@words|
let ifs: Option<String> = std::env::var("_CLAP_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, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(buf, "{}", completion.to_string_lossy())?;
write!(buf, "{}", candidate.get_content().to_string_lossy())?;
}
Ok(())
}
Expand Down
6 changes: 3 additions & 3 deletions clap_complete/src/dynamic/shells/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ impl crate::dynamic::Completer for Fish {
let index = args.len() - 1;
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;

for (completion, help) in completions {
write!(buf, "{}", completion.to_string_lossy())?;
if let Some(help) = help {
for candidate in completions {
write!(buf, "{}", candidate.get_content().to_string_lossy())?;
if let Some(help) = candidate.get_help() {
write!(
buf,
"\t{}",
Expand Down
4 changes: 2 additions & 2 deletions clap_complete/src/dynamic/shells/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ compdef _clap_dynamic_completer BIN"#
}
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;

for (i, (completion, _)) in completions.iter().enumerate() {
for (i, candidate) in completions.iter().enumerate() {
if i != 0 {
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
}
write!(buf, "{}", completion.to_string_lossy())?;
write!(buf, "{}", candidate.get_content().to_string_lossy())?;
}
Ok(())
}
Expand Down
Loading

0 comments on commit 8710057

Please sign in to comment.