blob: 71c586d8e1c67b96e0690e0916db01b0aeedf3ab [file] [log] [blame]
use std::io::Write;
use clap::*;
use crate::generator::{utils, Generator};
use crate::INTERNAL_ERROR_MSG;
/// Generate zsh completion file
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Zsh;
impl Generator for Zsh {
fn file_name(&self, name: &str) -> String {
format!("_{name}")
}
fn generate(&self, cmd: &Command, buf: &mut dyn Write) {
let bin_name = cmd
.get_bin_name()
.expect("crate::generate should have set the bin_name");
w!(
buf,
format!(
"#compdef {name}
autoload -U is-at-least
_{name}() {{
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext=\"$curcontext\" state line
{initial_args}{subcommands}
}}
{subcommand_details}
if [ \"$funcstack[1]\" = \"_{name}\" ]; then
_{name} \"$@\"
else
compdef _{name} {name}
fi
",
name = bin_name,
initial_args = get_args_of(cmd, None),
subcommands = get_subcommands_of(cmd),
subcommand_details = subcommand_details(cmd)
)
.as_bytes()
);
}
}
// Displays the commands of a subcommand
// (( $+functions[_[bin_name_underscore]_commands] )) ||
// _[bin_name_underscore]_commands() {
// local commands; commands=(
// '[arg_name]:[arg_help]'
// )
// _describe -t commands '[bin_name] commands' commands "$@"
//
// Where the following variables are present:
// [bin_name_underscore]: The full space delineated bin_name, where spaces have been replaced by
// underscore characters
// [arg_name]: The name of the subcommand
// [arg_help]: The help message of the subcommand
// [bin_name]: The full space delineated bin_name
//
// Here's a snippet from rustup:
//
// (( $+functions[_rustup_commands] )) ||
// _rustup_commands() {
// local commands; commands=(
// 'show:Show the active and installed toolchains'
// 'update:Update Rust toolchains'
// # ... snip for brevity
// 'help:Print this message or the help of the given subcommand(s)'
// )
// _describe -t commands 'rustup commands' commands "$@"
//
fn subcommand_details(p: &Command) -> String {
debug!("subcommand_details");
let bin_name = p
.get_bin_name()
.expect("crate::generate should have set the bin_name");
let mut ret = vec![];
// First we do ourself
let parent_text = format!(
"\
(( $+functions[_{bin_name_underscore}_commands] )) ||
_{bin_name_underscore}_commands() {{
local commands; commands=({subcommands_and_args})
_describe -t commands '{bin_name} commands' commands \"$@\"
}}",
bin_name_underscore = bin_name.replace(' ', "__"),
bin_name = bin_name,
subcommands_and_args = subcommands_of(p)
);
ret.push(parent_text);
// Next we start looping through all the children, grandchildren, etc.
let mut all_subcommands = utils::all_subcommands(p);
all_subcommands.sort();
all_subcommands.dedup();
for (_, ref bin_name) in &all_subcommands {
debug!("subcommand_details:iter: bin_name={bin_name}");
ret.push(format!(
"\
(( $+functions[_{bin_name_underscore}_commands] )) ||
_{bin_name_underscore}_commands() {{
local commands; commands=({subcommands_and_args})
_describe -t commands '{bin_name} commands' commands \"$@\"
}}",
bin_name_underscore = bin_name.replace(' ', "__"),
bin_name = bin_name,
subcommands_and_args =
subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG))
));
}
ret.join("\n")
}
// Generates subcommand completions in form of
//
// '[arg_name]:[arg_help]'
//
// Where:
// [arg_name]: the subcommand's name
// [arg_help]: the help message of the subcommand
//
// A snippet from rustup:
// 'show:Show the active and installed toolchains'
// 'update:Update Rust toolchains'
fn subcommands_of(p: &Command) -> String {
debug!("subcommands_of");
let mut segments = vec![];
fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec<String>) {
debug!("add_subcommands");
let text = format!(
"'{name}:{help}' \\",
name = name,
help = escape_help(&subcommand.get_about().unwrap_or_default().to_string())
);
ret.push(text);
}
// The subcommands
for command in p.get_subcommands() {
debug!("subcommands_of:iter: subcommand={}", command.get_name());
add_subcommands(command, command.get_name(), &mut segments);
for alias in command.get_visible_aliases() {
add_subcommands(command, alias, &mut segments);
}
}
// Surround the text with newlines for proper formatting.
// We need this to prevent weirdly formatted `command=(\n \n)` sections.
// When there are no (sub-)commands.
if !segments.is_empty() {
segments.insert(0, "".to_string());
segments.push(" ".to_string());
}
segments.join("\n")
}
// Get's the subcommand section of a completion file
// This looks roughly like:
//
// case $state in
// ([bin_name]_args)
// curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\"
// case $line[1] in
//
// ([name])
// _arguments -C -s -S \
// [subcommand_args]
// && ret=0
//
// [RECURSIVE_CALLS]
//
// ;;",
//
// [repeat]
//
// esac
// ;;
// esac",
//
// Where the following variables are present:
// [name] = The subcommand name in the form of "install" for "rustup toolchain install"
// [bin_name] = The full space delineated bin_name such as "rustup toolchain install"
// [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens
// [repeat] = From the same recursive calls, but for all subcommands
// [subcommand_args] = The same as zsh::get_args_of
fn get_subcommands_of(parent: &Command) -> String {
debug!(
"get_subcommands_of: Has subcommands...{:?}",
parent.has_subcommands()
);
if !parent.has_subcommands() {
return String::new();
}
let subcommand_names = utils::subcommands(parent);
let mut all_subcommands = vec![];
for (ref name, ref bin_name) in &subcommand_names {
debug!(
"get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}",
parent.get_name(),
);
let mut segments = vec![format!("({name})")];
let subcommand_args = get_args_of(
parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG),
Some(parent),
);
if !subcommand_args.is_empty() {
segments.push(subcommand_args);
}
// Get the help text of all child subcommands.
let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG));
if !children.is_empty() {
segments.push(children);
}
segments.push(String::from(";;"));
all_subcommands.push(segments.join("\n"));
}
let parent_bin_name = parent
.get_bin_name()
.expect("crate::generate should have set the bin_name");
format!(
"
case $state in
({name})
words=($line[{pos}] \"${{words[@]}}\")
(( CURRENT += 1 ))
curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\"
case $line[{pos}] in
{subcommands}
esac
;;
esac",
name = parent.get_name(),
name_hyphen = parent_bin_name.replace(' ', "-"),
subcommands = all_subcommands.join("\n"),
pos = parent.get_positionals().count() + 1
)
}
// Get the Command for a given subcommand tree.
//
// Given the bin_name "a b c" and the Command for "a" this returns the "c" Command.
// Given the bin_name "a b c" and the Command for "b" this returns the "c" Command.
fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> {
debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name);
if bin_name == parent.get_bin_name().unwrap_or_default() {
return Some(parent);
}
for subcommand in parent.get_subcommands() {
if let Some(ret) = parser_of(subcommand, bin_name) {
return Some(ret);
}
}
None
}
// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to
// another ZSH function if there are subcommands.
// The structure works like this:
// ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)]
// ^-- list '-v -h' ^--'*' ^--'+' ^-- list 'one two three'
//
// An example from the rustup command:
//
// _arguments -C -s -S \
// '(-h --help --verbose)-v[Enable verbose output]' \
// '(-V -v --version --verbose --help)-h[Print help information]' \
// # ... snip for brevity
// ':: :_rustup_commands' \ # <-- displays subcommands
// '*::: :->rustup' \ # <-- displays subcommand args and child subcommands
// && ret=0
//
// The args used for _arguments are as follows:
// -C: modify the $context internal variable
// -s: Allow stacking of short args (i.e. -a -b -c => -abc)
// -S: Do not complete anything after '--' and treat those as argument values
fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String {
debug!("get_args_of");
let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" \\")];
let opts = write_opts_of(parent, p_global);
let flags = write_flags_of(parent, p_global);
let positionals = write_positionals_of(parent);
if !opts.is_empty() {
segments.push(opts);
}
if !flags.is_empty() {
segments.push(flags);
}
if !positionals.is_empty() {
segments.push(positionals);
}
if parent.has_subcommands() {
let parent_bin_name = parent
.get_bin_name()
.expect("crate::generate should have set the bin_name");
let subcommand_bin_name = format!(
"\":: :_{name}_commands\" \\",
name = parent_bin_name.replace(' ', "__")
);
segments.push(subcommand_bin_name);
let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name());
segments.push(subcommand_text);
};
segments.push(String::from("&& ret=0"));
segments.join("\n")
}
// Uses either `possible_vals` or `value_hint` to give hints about possible argument values
fn value_completion(arg: &Arg) -> Option<String> {
if let Some(values) = crate::generator::utils::possible_values(arg) {
if values
.iter()
.any(|value| !value.is_hide_set() && value.get_help().is_some())
{
Some(format!(
"(({}))",
values
.iter()
.filter_map(|value| {
if value.is_hide_set() {
None
} else {
Some(format!(
r#"{name}\:"{tooltip}""#,
name = escape_value(value.get_name()),
tooltip =
escape_help(&value.get_help().unwrap_or_default().to_string()),
))
}
})
.collect::<Vec<_>>()
.join("\n")
))
} else {
Some(format!(
"({})",
values
.iter()
.filter(|pv| !pv.is_hide_set())
.map(|n| n.get_name())
.collect::<Vec<_>>()
.join(" ")
))
}
} else {
// NB! If you change this, please also update the table in `ValueHint` documentation.
Some(
match arg.get_value_hint() {
ValueHint::Unknown => {
return None;
}
ValueHint::Other => "( )",
ValueHint::AnyPath => "_files",
ValueHint::FilePath => "_files",
ValueHint::DirPath => "_files -/",
ValueHint::ExecutablePath => "_absolute_command_paths",
ValueHint::CommandName => "_command_names -e",
ValueHint::CommandString => "_cmdstring",
ValueHint::CommandWithArguments => "_cmdambivalent",
ValueHint::Username => "_users",
ValueHint::Hostname => "_hosts",
ValueHint::Url => "_urls",
ValueHint::EmailAddress => "_email_addresses",
_ => {
return None;
}
}
.to_string(),
)
}
}
/// Escape help string inside single quotes and brackets
fn escape_help(string: &str) -> String {
string
.replace('\\', "\\\\")
.replace('\'', "'\\''")
.replace('[', "\\[")
.replace(']', "\\]")
.replace(':', "\\:")
.replace('$', "\\$")
.replace('`', "\\`")
}
/// Escape value string inside single quotes and parentheses
fn escape_value(string: &str) -> String {
string
.replace('\\', "\\\\")
.replace('\'', "'\\''")
.replace('[', "\\[")
.replace(']', "\\]")
.replace(':', "\\:")
.replace('$', "\\$")
.replace('`', "\\`")
.replace('(', "\\(")
.replace(')', "\\)")
.replace(' ', "\\ ")
}
fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String {
debug!("write_opts_of");
let mut ret = vec![];
for o in p.get_opts() {
debug!("write_opts_of:iter: o={}", o.get_id());
let help = escape_help(&o.get_help().unwrap_or_default().to_string());
let conflicts = arg_conflicts(p, o, p_global);
let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() {
"*"
} else {
""
};
let vn = match o.get_value_names() {
None => " ".to_string(),
Some(val) => val[0].to_string(),
};
let vc = match value_completion(o) {
Some(val) => format!(":{vn}:{val}"),
None => format!(":{vn}: "),
};
let vc = vc.repeat(o.get_num_args().expect("built").min_values());
if let Some(shorts) = o.get_short_and_visible_aliases() {
for short in shorts {
let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\");
debug!("write_opts_of:iter: Wrote...{}", &*s);
ret.push(s);
}
}
if let Some(longs) = o.get_long_and_visible_aliases() {
for long in longs {
let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\");
debug!("write_opts_of:iter: Wrote...{}", &*l);
ret.push(l);
}
}
}
ret.join("\n")
}
fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String {
fn push_conflicts(conflicts: &[&Arg], res: &mut Vec<String>) {
for conflict in conflicts {
if let Some(s) = conflict.get_short() {
res.push(format!("-{s}"));
}
if let Some(l) = conflict.get_long() {
res.push(format!("--{l}"));
}
}
}
let mut res = vec![];
match (app_global, arg.is_global_set()) {
(Some(x), true) => {
let conflicts = x.get_arg_conflicts_with(arg);
if conflicts.is_empty() {
return String::new();
}
push_conflicts(&conflicts, &mut res);
}
(_, _) => {
let conflicts = cmd.get_arg_conflicts_with(arg);
if conflicts.is_empty() {
return String::new();
}
push_conflicts(&conflicts, &mut res);
}
};
format!("({})", res.join(" "))
}
fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String {
debug!("write_flags_of;");
let mut ret = vec![];
for f in utils::flags(p) {
debug!("write_flags_of:iter: f={}", f.get_id());
let help = escape_help(&f.get_help().unwrap_or_default().to_string());
let conflicts = arg_conflicts(p, &f, p_global);
let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() {
"*"
} else {
""
};
if let Some(short) = f.get_short() {
let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\");
debug!("write_flags_of:iter: Wrote...{}", &*s);
ret.push(s);
if let Some(short_aliases) = f.get_visible_short_aliases() {
for alias in short_aliases {
let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",);
debug!("write_flags_of:iter: Wrote...{}", &*s);
ret.push(s);
}
}
}
if let Some(long) = f.get_long() {
let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\");
debug!("write_flags_of:iter: Wrote...{}", &*l);
ret.push(l);
if let Some(aliases) = f.get_visible_aliases() {
for alias in aliases {
let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\");
debug!("write_flags_of:iter: Wrote...{}", &*l);
ret.push(l);
}
}
}
}
ret.join("\n")
}
fn write_positionals_of(p: &Command) -> String {
debug!("write_positionals_of;");
let mut ret = vec![];
// Completions for commands that end with two Vec arguments require special care.
// - You can have two Vec args separated with a custom value terminator.
// - You can have two Vec args with the second one set to last (raw sets last)
// which will require a '--' separator to be used before the second argument
// on the command-line.
//
// We use the '-S' _arguments option to disable completion after '--'. Thus, the
// completion for the second argument in scenario (B) does not need to be emitted
// because it is implicitly handled by the '-S' option.
// We only need to emit the first catch-all.
//
// Have we already emitted a catch-all multi-valued positional argument
// without a custom value terminator?
let mut catch_all_emitted = false;
for arg in p.get_positionals() {
debug!("write_positionals_of:iter: arg={}", arg.get_id());
let num_args = arg.get_num_args().expect("built");
let is_multi_valued = num_args.max_values() > 1;
if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
// This is the final argument and it also takes multiple arguments.
// We've already emitted a catch-all positional argument so we don't need
// to emit anything for this argument because it is implicitly handled by
// the use of the '-S' _arguments option.
continue;
}
let cardinality_value;
let cardinality = if is_multi_valued {
match arg.get_value_terminator() {
Some(terminator) => {
cardinality_value = format!("*{}:", escape_value(terminator));
cardinality_value.as_str()
}
None => {
catch_all_emitted = true;
"*:"
}
}
} else if !arg.is_required_set() {
":"
} else {
""
};
let a = format!(
"'{cardinality}:{name}{help}:{value_completion}' \\",
cardinality = cardinality,
name = arg.get_id(),
help = arg
.get_help()
.map(|s| s.to_string())
.map(|v| " -- ".to_owned() + &v)
.unwrap_or_else(|| "".to_owned())
.replace('[', "\\[")
.replace(']', "\\]")
.replace('\'', "'\\''")
.replace(':', "\\:"),
value_completion = value_completion(arg).unwrap_or_default()
);
debug!("write_positionals_of:iter: Wrote...{a}");
ret.push(a);
}
ret.join("\n")
}
#[cfg(test)]
mod tests {
use crate::shells::zsh::{escape_help, escape_value};
#[test]
fn test_escape_value() {
let raw_string = "\\ [foo]() `bar https://$PATH";
assert_eq!(
escape_value(raw_string),
"\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
)
}
#[test]
fn test_escape_help() {
let raw_string = "\\ [foo]() `bar https://$PATH";
assert_eq!(
escape_help(raw_string),
"\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
)
}
}