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:
Kevin K 2016-10-23 19:32:52 -04:00
parent 291bcccd6b
commit 3e36b0bac4
No known key found for this signature in database
GPG key ID: 17218E4B3692F01A
3 changed files with 457 additions and 5 deletions

View file

@ -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
View 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
View 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")
}