diff options
Diffstat (limited to 'src/shells/zsh.rs')
-rw-r--r-- | src/shells/zsh.rs | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/src/shells/zsh.rs b/src/shells/zsh.rs new file mode 100644 index 0000000..65d7af6 --- /dev/null +++ b/src/shells/zsh.rs @@ -0,0 +1,691 @@ +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; + // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't + // emit one here. + let cardinality = if is_multi_valued && !p.has_subcommands() { + 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" + ) + } +} |