mirror of
https://github.com/clap-rs/clap
synced 2025-03-04 15:27:16 +00:00
feat(Completions): adds ZSH completion support
ZSH is now supported in the exact same way as BASH and FISH completion scripts. Closes #699
This commit is contained in:
parent
291bcccd6b
commit
3e36b0bac4
3 changed files with 457 additions and 5 deletions
|
@ -116,12 +116,15 @@ impl<'a, 'b> Parser<'a, 'b>
|
|||
use std::error::Error;
|
||||
|
||||
let out_dir = PathBuf::from(od);
|
||||
let suffix = match for_shell {
|
||||
Shell::Bash => ".bash-completion",
|
||||
Shell::Fish => ".fish",
|
||||
let name = &*self.meta.bin_name.as_ref().unwrap().clone();
|
||||
let file_name = match for_shell {
|
||||
|
||||
Shell::Bash => format!("{}.bash-completion", name),
|
||||
Shell::Fish => format!("{}.fish", name),
|
||||
Shell::Zsh => format!("_{}", name)
|
||||
};
|
||||
|
||||
let mut file = match File::create(out_dir.join(format!("{}{}", &*self.meta.bin_name.as_ref().unwrap(), suffix))) {
|
||||
let mut file = match File::create(out_dir.join(file_name)) {
|
||||
Err(why) => panic!("couldn't create bash completion file: {}",
|
||||
why.description()),
|
||||
Ok(file) => file,
|
||||
|
@ -276,7 +279,6 @@ impl<'a, 'b> Parser<'a, 'b>
|
|||
self.required.iter()
|
||||
}
|
||||
|
||||
|
||||
pub fn get_required_from(&self,
|
||||
reqs: &[&'a str],
|
||||
matcher: Option<&ArgMatcher<'a>>)
|
||||
|
@ -1967,6 +1969,40 @@ impl<'a, 'b> Parser<'a, 'b>
|
|||
ColorWhen::Auto
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_arg(&self, arg: &str) -> Option<&AnyArg> {
|
||||
for f in self.flags() {
|
||||
if f.name == arg {
|
||||
return Some(f);
|
||||
}
|
||||
}
|
||||
for o in self.opts() {
|
||||
if o.name == arg {
|
||||
return Some(o);
|
||||
}
|
||||
}
|
||||
for p in self.positionals() {
|
||||
if p.name == arg {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_subcommand(&'b self, sc: &str) -> Option<&'b App<'a, 'b>> {
|
||||
debugln!("fn=find_subcommand;");
|
||||
debugln!("Looking for sc...{}", sc);
|
||||
debugln!("Currently in Parser...{}", self.meta.bin_name.as_ref().unwrap());
|
||||
for s in self.subcommands.iter() {
|
||||
if s.p.meta.bin_name.as_ref().unwrap_or(&String::new()) == sc {
|
||||
return Some(s);
|
||||
}
|
||||
if let Some(app) = s.p.find_subcommand(sc) {
|
||||
return Some(app);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Clone for Parser<'a, 'b>
|
||||
|
|
52
src/completions/shell.rs
Normal file
52
src/completions/shell.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use std::ascii::AsciiExt;
|
||||
use std::str::FromStr;
|
||||
use std::fmt;
|
||||
|
||||
/// Describes which shell to produce a completions file for
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Shell {
|
||||
/// Generates a .bash-completion completion file for the Bourne Again SHell (BASH)
|
||||
Bash,
|
||||
/// Generates a .fish completion file for the Friendly Interactive SHell (fish)
|
||||
Fish,
|
||||
/// Generates a completion file for the Z SHell (ZSH)
|
||||
Zsh,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// A list of possible variants in `&'static str` form
|
||||
pub fn variants() -> [&'static str; 3] {
|
||||
[
|
||||
"zsh",
|
||||
"bash",
|
||||
"fish"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Shell {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
|
||||
"ZSH" | _ if s.eq_ignore_ascii_case("zsh") => Ok(Shell::Zsh),
|
||||
"FISH" | _ if s.eq_ignore_ascii_case("fish") => Ok(Shell::Fish),
|
||||
"BASH" | _ if s.eq_ignore_ascii_case("bash") => Ok(Shell::Bash),
|
||||
_ => Err(
|
||||
String::from("[valid values: bash, fish, zsh]")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Shell {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Shell::Bash => write!(f, "BASH"),
|
||||
Shell::Fish => write!(f, "FISH"),
|
||||
Shell::Zsh => write!(f, "ZSH"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
364
src/completions/zsh.rs
Normal file
364
src/completions/zsh.rs
Normal file
|
@ -0,0 +1,364 @@
|
|||
|
||||
// Std
|
||||
use std::io::Write;
|
||||
use std::ascii::AsciiExt;
|
||||
|
||||
// Internal
|
||||
use app::parser::Parser;
|
||||
use args::{ArgSettings, AnyArg};
|
||||
use completions;
|
||||
use INTERNAL_ERROR_MSG;
|
||||
|
||||
pub struct ZshGen<'a, 'b>
|
||||
where 'a: 'b
|
||||
{
|
||||
p: &'b Parser<'a, 'b>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> ZshGen<'a, 'b> {
|
||||
pub fn new(p: &'b Parser<'a, 'b>) -> Self {
|
||||
debugln!("fn=ZshGen::new;");
|
||||
ZshGen {
|
||||
p: p,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_to<W: Write>(&self, buf: &mut W) {
|
||||
debugln!("fn=generate_to;");
|
||||
w!(buf,
|
||||
format!("\
|
||||
#compdef {name}
|
||||
|
||||
_{name}() {{
|
||||
typeset -A opt_args
|
||||
local ret=1
|
||||
|
||||
local context curcontext=\"$curcontext\" state line
|
||||
{initial_args}
|
||||
{subcommands}
|
||||
}}
|
||||
|
||||
{subcommand_details}
|
||||
|
||||
_{name} \"$@\"",
|
||||
name = self.p.meta.bin_name.as_ref().unwrap(),
|
||||
initial_args = get_args_of(self.p),
|
||||
subcommands = get_subcommands_of(self.p),
|
||||
subcommand_details = subcommand_details(self.p)).as_bytes());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Displays the positional args and 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 deliniated bin_name, where spaces have been replaced by
|
||||
// underscore characters
|
||||
// [arg_name]: The name of the positional arg or subcommand
|
||||
// [arg_help]: The help message of the arg or subcommand
|
||||
// [bin_name]: The full space deliniated 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:Prints this message or the help of the given subcommand(s)'
|
||||
// )
|
||||
// _describe -t commands 'rustup commands' commands "$@"
|
||||
//
|
||||
fn subcommand_details(p: &Parser) -> String {
|
||||
debugln!("fn=subcommand_details");
|
||||
// First we do ourself
|
||||
let mut ret = vec![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 = p.meta.bin_name.as_ref().unwrap().replace(" ", "_"),
|
||||
bin_name = p.meta.bin_name.as_ref().unwrap(),
|
||||
subcommands_and_args = subcommands_and_args_of(p))];
|
||||
|
||||
// Next we start looping through all the children, grandchildren, etc.
|
||||
let mut all_subcommands = completions::all_subcommands(p);
|
||||
all_subcommands.sort();
|
||||
all_subcommands.dedup();
|
||||
for &(_, ref bin_name) in &all_subcommands {
|
||||
debugln!("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_and_args_of(parser_of(p, bin_name))));
|
||||
}
|
||||
|
||||
ret.join("\n")
|
||||
}
|
||||
|
||||
// Generates subcommand and positional argument completions in form of
|
||||
//
|
||||
// '[arg_name]:[arg_help]'
|
||||
//
|
||||
// Where:
|
||||
// [arg_name]: the argument or subcommand's name
|
||||
// [arg_help]: the help message of the argument or subcommand
|
||||
//
|
||||
// A snippet from rustup:
|
||||
// 'show:Show the active and installed toolchains'
|
||||
// 'update:Update Rust toolchains'
|
||||
fn subcommands_and_args_of(p: &Parser) -> String {
|
||||
debugln!("fn=subcommands_and_args_of;");
|
||||
let mut ret = vec![];
|
||||
|
||||
// Firs the subcommands
|
||||
for sc in p.subcommands() {
|
||||
debugln!("iter;subcommand={}", sc.p.meta.name);
|
||||
let s = format!("\"{name}:{help}\" \\",
|
||||
name = sc.p.meta.name,
|
||||
help = sc.p.meta.about.unwrap_or(""));
|
||||
if !s.is_empty() {
|
||||
ret.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Then the positional args
|
||||
for arg in p.positionals() {
|
||||
debugln!("iter;arg={}", arg.name);
|
||||
let a = format!("\"{name}:{help}\" \\",
|
||||
name = arg.name.to_ascii_uppercase(),
|
||||
help = arg.help.unwrap_or(""));
|
||||
|
||||
if !a.is_empty() {
|
||||
ret.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
ret.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 deliniated bin_name such as "rustup toolchain install"
|
||||
// [name_hyphen] = The full space deliniated 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(p: &Parser) -> String {
|
||||
debugln!("fn=get_subcommands");
|
||||
|
||||
debug!("Has subcommands...");
|
||||
if !p.has_subcommands() {
|
||||
sdebugln!("No");
|
||||
return String::new();
|
||||
} else {
|
||||
sdebugln!("Yes");
|
||||
}
|
||||
|
||||
let sc_names = completions::subcommands_of(p);
|
||||
|
||||
let mut subcmds = vec![];
|
||||
for &(ref name, ref bin_name) in &sc_names {
|
||||
let mut v = vec![format!("({})", name)];
|
||||
let subcommand_args = get_args_of(parser_of(p, &*bin_name));
|
||||
if !subcommand_args.is_empty() {
|
||||
v.push(subcommand_args);
|
||||
}
|
||||
let subcommands = get_subcommands_of(parser_of(p, &*bin_name));
|
||||
if !subcommands.is_empty() {
|
||||
v.push(subcommands);
|
||||
}
|
||||
v.push(String::from(";;"));
|
||||
subcmds.push(v.join("\n"));
|
||||
}
|
||||
|
||||
format!(
|
||||
"case $state in
|
||||
({name})
|
||||
curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$words[1]:\"
|
||||
case $line[1] in
|
||||
{subcommands}
|
||||
esac
|
||||
;;
|
||||
esac",
|
||||
name = p.meta.name,
|
||||
name_hyphen = p.meta.bin_name.as_ref().unwrap().replace(" ", "-"),
|
||||
subcommands = subcmds.join("\n"))
|
||||
}
|
||||
|
||||
fn parser_of<'a, 'b>(p: &'b Parser<'a, 'b>, sc: &str) -> &'b Parser<'a, 'b> {
|
||||
debugln!("fn=parser_of;sc={}", sc);
|
||||
&p.find_subcommand(sc).expect(INTERNAL_ERROR_MSG).p
|
||||
}
|
||||
|
||||
// Writes out the args section, which ends up being the flags and opts, and a jump to
|
||||
// another ZSH function if there are positional args or subcommands.
|
||||
// The structer 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[Prints help information]' \
|
||||
// # ... snip for brevity
|
||||
// '1:: :_rustup_commands' \ # <-- displays positional args and 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(p: &Parser) -> String {
|
||||
debugln!("fn=get_args_of");
|
||||
let mut ret = vec![String::from("_arguments -s -S -C \\")];
|
||||
let opts = write_opts_of(p);
|
||||
let flags = write_flags_of(p);
|
||||
let sc_or_a = if p.has_subcommands() || p.has_positionals() {
|
||||
format!("\"1:: :_{name}_commands\" \\",
|
||||
name = p.meta.bin_name.as_ref().unwrap().replace(" ", "_"))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let sc = if p.has_subcommands() {
|
||||
format!("\"*:: :->{name}\" \\", name = p.meta.name)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if !opts.is_empty() {
|
||||
ret.push(opts);
|
||||
}
|
||||
if !flags.is_empty() {
|
||||
ret.push(flags);
|
||||
}
|
||||
if !sc_or_a.is_empty() {
|
||||
ret.push(sc_or_a);
|
||||
}
|
||||
if !sc.is_empty() {
|
||||
ret.push(sc);
|
||||
}
|
||||
ret.push(String::from("&& ret=0"));
|
||||
|
||||
ret.join("\n")
|
||||
}
|
||||
|
||||
fn write_opts_of(p: &Parser) -> String {
|
||||
debugln!("fn=write_opts_of;");
|
||||
let mut ret = vec![];
|
||||
for o in p.opts() {
|
||||
debugln!("iter;o={}", o.name());
|
||||
let help = o.help().unwrap_or("");
|
||||
let mut conflicts = get_zsh_arg_conflicts!(p, o, INTERNAL_ERROR_MSG);
|
||||
conflicts = if conflicts.is_empty() { String::new() } else { format!("({})", conflicts) };
|
||||
|
||||
let multiple = if o.is_set(ArgSettings::Multiple) { "*" } else { "" };
|
||||
let pv = format!("{}",
|
||||
if let Some(pv_vec) = o.possible_vals() {
|
||||
format!(": :({})", pv_vec.join(" "))
|
||||
} else {
|
||||
String::new()
|
||||
});
|
||||
if let Some(short) = o.short() {
|
||||
let s = format!("\"{conflicts}{multiple}-{arg}+[{help}]{possible_values}\" \\",
|
||||
conflicts = conflicts,
|
||||
multiple = multiple,
|
||||
arg = short,
|
||||
possible_values = pv,
|
||||
help = help);
|
||||
|
||||
ret.push(s);
|
||||
debugln!("Wrote...{}", &*s);
|
||||
}
|
||||
if let Some(long) = o.long() {
|
||||
let l = format!("\"{conflicts}{multiple}--{arg}+[{help}]{possible_values}\" \\",
|
||||
conflicts = conflicts,
|
||||
multiple = multiple,
|
||||
arg = long,
|
||||
possible_values = pv,
|
||||
help = help);
|
||||
|
||||
ret.push(l);
|
||||
debugln!("Wrote...{}", &*l);
|
||||
}
|
||||
}
|
||||
|
||||
ret.join("\n")
|
||||
}
|
||||
|
||||
fn write_flags_of(p: &Parser) -> String {
|
||||
debugln!("fn=write_flags_of;");
|
||||
let mut ret = vec![];
|
||||
for f in p.flags() {
|
||||
debugln!("iter;f={}", f.name());
|
||||
let help = f.help().unwrap_or("");
|
||||
let mut conflicts = get_zsh_arg_conflicts!(p, f, INTERNAL_ERROR_MSG);
|
||||
conflicts = if conflicts.is_empty() { String::new() } else { format!("({})", conflicts) };
|
||||
|
||||
let multiple = if f.is_set(ArgSettings::Multiple) { "*" } else { "" };
|
||||
if let Some(short) = f.short() {
|
||||
let s = format!("\"{conflicts}{multiple}-{arg}[{help}]\" \\",
|
||||
multiple = multiple,
|
||||
conflicts = conflicts,
|
||||
arg = short,
|
||||
help = help);
|
||||
|
||||
ret.push(s);
|
||||
debugln!("Wrote...{}", &*s);
|
||||
}
|
||||
|
||||
if let Some(long) = f.long() {
|
||||
let l = format!("\"{conflicts}{multiple}--{arg}[{help}]\" \\",
|
||||
conflicts = conflicts,
|
||||
multiple = multiple,
|
||||
arg = long,
|
||||
help = help);
|
||||
|
||||
ret.push(l);
|
||||
debugln!("Wrote...{}", &*l);
|
||||
}
|
||||
}
|
||||
|
||||
ret.join("\n")
|
||||
}
|
Loading…
Add table
Reference in a new issue