From 33f47acc67a0aa41224a7ffdb2f68d665726d6a7 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Sunkara Date: Wed, 5 Feb 2020 11:04:59 +0100 Subject: [PATCH 1/3] Refactor clap_generate --- Cargo.toml | 5 +- clap_derive/Cargo.toml | 1 + clap_derive/README.md | 7 +- clap_generate/Cargo.toml | 50 + clap_generate/README.md | 1 + clap_generate/src/generators/mod.rs | 215 +++++ clap_generate/src/generators/shells/bash.rs | 212 +++++ clap_generate/src/generators/shells/elvish.rs | 133 +++ clap_generate/src/generators/shells/fish.rs | 113 +++ clap_generate/src/generators/shells/mod.rs | 11 + .../src/generators/shells/powershell.rs | 171 ++++ clap_generate/src/generators/shells/zsh.rs | 526 +++++++++++ clap_generate/src/lib.rs | 188 ++++ clap_generate/src/macros.rs | 87 ++ clap_generate/tests/completions.rs | 858 ++++++++++++++++++ clap_generate/tests/version-numbers.rs | 6 + src/lib.rs | 5 +- src/macros.rs | 30 +- src/mkeymap.rs | 1 + 19 files changed, 2603 insertions(+), 17 deletions(-) create mode 100644 clap_generate/Cargo.toml create mode 100644 clap_generate/README.md create mode 100644 clap_generate/src/generators/mod.rs create mode 100644 clap_generate/src/generators/shells/bash.rs create mode 100644 clap_generate/src/generators/shells/elvish.rs create mode 100644 clap_generate/src/generators/shells/fish.rs create mode 100644 clap_generate/src/generators/shells/mod.rs create mode 100644 clap_generate/src/generators/shells/powershell.rs create mode 100644 clap_generate/src/generators/shells/zsh.rs create mode 100644 clap_generate/src/lib.rs create mode 100644 clap_generate/src/macros.rs create mode 100644 clap_generate/tests/completions.rs create mode 100644 clap_generate/tests/version-numbers.rs diff --git a/Cargo.toml b/Cargo.toml index b4b40c35..610a5b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,10 @@ features = ["doc"] [workspace] members = [ "clap_derive", + "clap_generate", ] default-members = [ - ".", "clap_derive", + ".", + "clap_derive", + "clap_generate", ] diff --git a/clap_derive/Cargo.toml b/clap_derive/Cargo.toml index 489c031d..a4734eef 100644 --- a/clap_derive/Cargo.toml +++ b/clap_derive/Cargo.toml @@ -46,6 +46,7 @@ proc-macro-error = "0.4.3" clap = { path = "../", version = "3.0.0-beta.1" } trybuild = "1.0.5" rustversion = "1" +version-sync = "0.8" [features] default = [] diff --git a/clap_derive/README.md b/clap_derive/README.md index ff89f477..68851041 100644 --- a/clap_derive/README.md +++ b/clap_derive/README.md @@ -1,9 +1,4 @@ -# Work in Progress - -This crate is currently a work in progress and not meant to be used. Please use [`structopt`](https://github.com/TeXitoi/structopt) -while this crate is being built. - -# clap_derive[![Build status](https://travis-ci.org/clap-rs/clap_derive.svg?branch=master)](https://travis-ci.org/clap-rs/clap_derive) [![](https://img.shields.io/crates/v/clap_derive.svg)](https://crates.io/crates/clap_derive) [![](https://docs.rs/clap_derive/badge.svg)](https://docs.rs/clap_derive) +# clap_derive Parse command line argument by defining a struct. It combines [structopt](https://github.com/TeXitoi/structopt) and [clap](https://crates.io/crates/clap) into a single experience. This crate is used by clap, and not meant to be used directly by consumers. diff --git a/clap_generate/Cargo.toml b/clap_generate/Cargo.toml new file mode 100644 index 00000000..e7caa6c1 --- /dev/null +++ b/clap_generate/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "clap_generate" +version = "3.0.0-beta.1" +edition = "2018" +authors = [ + "Kevin K. ", + "Pavan Kumar Sunkara ", +] +include = [ + "src/**/*", + "Cargo.toml", + "README.md" +] +description = "A generator library used with clap for shell completion scripts, manpage, etc." +repository = "https://github.com/clap-rs/clap/tree/master/clap_generate" +documentation = "https://docs.rs/clap_generate" +homepage = "https://clap.rs/" +keywords = [ + "clap", + "cli", + "generate", + "completion", + "manpage", + "parse" +] +categories = ["command-line-interface"] +license = "MIT OR Apache-2.0" +readme = "README.md" + +[badges] +is-it-maintained-issue-resolution = { repository = "clap-rs/clap" } +is-it-maintained-open-issues = { repository = "clap-rs/clap" } +maintenance = {status = "actively-developed"} + +[dependencies] +clap = { path = "../", version = "3.0.0-beta.1" } + +[dev-dependencies] +pretty_assertions = "0.6" +version-sync = "0.8" + +[features] +default = [] +unstable = ["clap/unstable"] +nightly = ["clap/nightly"] +debug = ["clap/debug"] +doc = [] + +[package.metadata.docs.rs] +features = ["doc"] diff --git a/clap_generate/README.md b/clap_generate/README.md new file mode 100644 index 00000000..17ce782f --- /dev/null +++ b/clap_generate/README.md @@ -0,0 +1 @@ +# clap_generate diff --git a/clap_generate/src/generators/mod.rs b/clap_generate/src/generators/mod.rs new file mode 100644 index 00000000..c04f09f2 --- /dev/null +++ b/clap_generate/src/generators/mod.rs @@ -0,0 +1,215 @@ +mod shells; + +// Std +use std::io::Write; + +// Internal +use clap::*; + +pub use shells::*; + +/// Generator trait which can be used to write generators +pub trait Generator { + /// Returns the file name that is created when this generator is called during compile time. + /// + /// # Examples + /// + /// ``` + /// # use std::io::Write; + /// # use clap::App; + /// use clap_generate::Generator; + /// + /// pub struct Fish; + /// + /// impl Generator for Fish { + /// # fn generate(app: &App, buf: &mut dyn Write) {} + /// fn file_name(name: &str) -> String { + /// format!("{}.fish", name) + /// } + /// } + /// ``` + fn file_name(name: &str) -> String; + + /// Generates output out of [`clap::App`](../clap/struct.App.html). + /// + /// # Examples + /// + /// The following example generator displays the [`clap::App`](../clap/struct.App.html) + /// as if it is printed using [`std::println`](https://doc.rust-lang.org/std/macro.println.html). + /// + /// ``` + /// use std::{io::Write, fmt::write}; + /// use clap::App; + /// use clap_generate::Generator; + /// + /// pub struct ClapDebug; + /// + /// impl Generator for ClapDebug { + /// fn generate(app: &App, buf: &mut dyn Write) { + /// write!(buf, "{}", app).unwrap(); + /// } + /// # fn file_name(name: &str) -> String { + /// # name.into() + /// # } + /// } + /// ``` + fn generate(app: &App, buf: &mut dyn Write); + + /// Gets all subcommands including child subcommands in the form of 'name' where the name + /// is a single word (i.e. "install") of the path to said subcommand (i.e. + /// "rustup toolchain install") + /// + /// Also note, aliases are treated as their own subcommands but duplicates of whatever they're + /// aliasing. + fn all_subcommand_names(app: &App) -> Vec { + debugln!("all_subcommand_names;"); + + let mut subcmds: Vec<_> = Self::subcommands_of(app) + .iter() + .map(|&(ref n, _)| n.clone()) + .collect(); + + for sc_v in subcommands!(app).map(|s| Self::all_subcommand_names(&s)) { + subcmds.extend(sc_v); + } + + subcmds.sort(); + subcmds.dedup(); + subcmds + } + + /// Gets all subcommands including child subcommands in the form of ('name', 'bin_name') where the name + /// is a single word (i.e. "install") of the path and full bin_name of said subcommand (i.e. + /// "rustup toolchain install") + /// + /// Also note, aliases are treated as their own subcommands but duplicates of whatever they're + /// aliasing. + fn all_subcommands(app: &App) -> Vec<(String, String)> { + debugln!("all_subcommands;"); + + let mut subcmds: Vec<_> = Self::subcommands_of(app); + + for sc_v in subcommands!(app).map(|s| Self::all_subcommands(&s)) { + subcmds.extend(sc_v); + } + + subcmds + } + + /// Gets all subcommands exlcuding child subcommands in the form of (name, bin_name) where the name + /// is a single word (i.e. "install") and the bin_name is a space deliniated list of the path to said + /// subcommand (i.e. "rustup toolchain install") + /// + /// Also note, aliases are treated as their own subcommands but duplicates of whatever they're + /// aliasing. + fn subcommands_of(p: &App) -> Vec<(String, String)> { + debugln!( + "subcommands_of: name={}, bin_name={}", + p.name, + p.bin_name.as_ref().unwrap() + ); + debugln!( + "subcommands_of: Has subcommands...{:?}", + p.has_subcommands() + ); + + let mut subcmds = vec![]; + + if !p.has_subcommands() { + let mut ret = vec![]; + + debugln!("subcommands_of: Looking for aliases..."); + + if let Some(ref aliases) = p.aliases { + for &(n, _) in aliases { + debugln!("subcommands_of:iter:iter: Found alias...{}", n); + + let mut als_bin_name: Vec<_> = + p.bin_name.as_ref().unwrap().split(' ').collect(); + + als_bin_name.push(n); + + let old = als_bin_name.len() - 2; + + als_bin_name.swap_remove(old); + ret.push((n.to_owned(), als_bin_name.join(" "))); + } + } + + return ret; + } + + for sc in subcommands!(p) { + debugln!( + "subcommands_of:iter: name={}, bin_name={}", + sc.name, + sc.bin_name.as_ref().unwrap() + ); + debugln!("subcommands_of:iter: Looking for aliases..."); + + if let Some(ref aliases) = sc.aliases { + for &(n, _) in aliases { + debugln!("subcommands_of:iter:iter: Found alias...{}", n); + + let mut als_bin_name: Vec<_> = + p.bin_name.as_ref().unwrap().split(' ').collect(); + + als_bin_name.push(n); + + let old = als_bin_name.len() - 2; + + als_bin_name.swap_remove(old); + subcmds.push((n.to_owned(), als_bin_name.join(" "))); + } + } + + subcmds.push((sc.name.clone(), sc.get_bin_name().unwrap().to_string())); + } + + subcmds + } + + /// TODO + fn get_all_subcommand_paths(p: &App, first: bool) -> Vec { + debugln!("get_all_subcommand_paths;"); + + let mut subcmds = vec![]; + + if !p.has_subcommands() { + if !first { + let name = &*p.name; + let path = p.get_bin_name().unwrap().to_string().replace(" ", "__"); + let mut ret = vec![path.clone()]; + + if let Some(ref aliases) = p.aliases { + for &(n, _) in aliases { + ret.push(path.replace(name, n)); + } + } + + return ret; + } + + return vec![]; + } + + for sc in subcommands!(p) { + let name = &*sc.name; + let path = sc.get_bin_name().unwrap().to_string().replace(" ", "__"); + + subcmds.push(path.clone()); + + if let Some(ref aliases) = sc.aliases { + for &(n, _) in aliases { + subcmds.push(path.replace(name, n)); + } + } + } + + for sc_v in subcommands!(p).map(|s| Self::get_all_subcommand_paths(&s, false)) { + subcmds.extend(sc_v); + } + + subcmds + } +} diff --git a/clap_generate/src/generators/shells/bash.rs b/clap_generate/src/generators/shells/bash.rs new file mode 100644 index 00000000..ef4efca8 --- /dev/null +++ b/clap_generate/src/generators/shells/bash.rs @@ -0,0 +1,212 @@ +// Std +use std::io::Write; + +// Internal +use crate::Generator; +use clap::*; + +/// Generate bash completion file +pub struct Bash; + +impl Generator for Bash { + fn file_name(name: &str) -> String { + format!("{}.bash", name) + } + + fn generate(app: &App, buf: &mut dyn Write) { + let bin_name = app.get_bin_name().unwrap(); + + w!( + buf, + format!( + "_{name}() {{ + local i cur prev opts cmds + COMPREPLY=() + cur=\"${{COMP_WORDS[COMP_CWORD]}}\" + prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\" + cmd=\"\" + opts=\"\" + + for i in ${{COMP_WORDS[@]}} + do + case \"${{i}}\" in + {name}) + cmd=\"{name}\" + ;; + {subcmds} + *) + ;; + esac + done + + case \"${{cmd}}\" in + {name}) + opts=\"{name_opts}\" + if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W \"${{opts}}\" -- ${{cur}}) ) + return 0 + fi + case \"${{prev}}\" in + {name_opts_details} + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W \"${{opts}}\" -- ${{cur}}) ) + return 0 + ;; + {subcmd_details} + esac +}} + +complete -F _{name} -o bashdefault -o default {name} +", + name = bin_name, + name_opts = all_options_for_path(app, bin_name), + name_opts_details = option_details_for_path(app, bin_name), + subcmds = all_subcommands(app), + subcmd_details = subcommand_details(app) + ) + .as_bytes() + ); + } +} + +fn all_subcommands(app: &App) -> String { + debugln!("Bash::all_subcommands;"); + + let mut subcmds = String::new(); + let scs = Bash::all_subcommand_names(app); + + for sc in &scs { + subcmds = format!( + "{} + {name}) + cmd+=\"__{fn_name}\" + ;;", + subcmds, + name = sc, + fn_name = sc.replace("-", "__") + ); + } + + subcmds +} + +fn subcommand_details(app: &App) -> String { + debugln!("Bash::subcommand_details;"); + + let mut subcmd_dets = String::new(); + let mut scs = Bash::get_all_subcommand_paths(app, true); + + scs.sort(); + scs.dedup(); + + for sc in &scs { + subcmd_dets = format!( + "{} + {subcmd}) + opts=\"{sc_opts}\" + if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then + COMPREPLY=( $(compgen -W \"${{opts}}\" -- ${{cur}}) ) + return 0 + fi + case \"${{prev}}\" in + {opts_details} + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W \"${{opts}}\" -- ${{cur}}) ) + return 0 + ;;", + subcmd_dets, + subcmd = sc.replace("-", "__"), + sc_opts = all_options_for_path(app, &*sc), + level = sc.split("__").map(|_| 1).fold(0, |acc, n| acc + n), + opts_details = option_details_for_path(app, &*sc) + ); + } + + subcmd_dets +} + +fn option_details_for_path(app: &App, path: &str) -> String { + debugln!("Bash::option_details_for_path: path={}", path); + + let mut p = app; + + for sc in path.split("__").skip(1) { + debugln!("Bash::option_details_for_path:iter: sc={}", sc); + p = &find_subcmd!(p, sc).unwrap(); + } + + let mut opts = String::new(); + + for o in opts!(p) { + if let Some(l) = o.long { + opts = format!( + "{} + --{}) + COMPREPLY=({}) + return 0 + ;;", + opts, + l, + vals_for(o) + ); + } + + if let Some(s) = o.short { + opts = format!( + "{} + -{}) + COMPREPLY=({}) + return 0 + ;;", + opts, + s, + vals_for(o) + ); + } + } + + opts +} + +fn vals_for(o: &Arg) -> String { + debugln!("Bash::vals_for: o={}", o.name); + + if let Some(ref vals) = o.possible_vals { + format!("$(compgen -W \"{}\" -- ${{cur}})", vals.join(" ")) + } else { + String::from("$(compgen -f ${cur})") + } +} + +fn all_options_for_path(app: &App, path: &str) -> String { + debugln!("Bash::all_options_for_path: path={}", path); + + let mut p = app; + + for sc in path.split("__").skip(1) { + debugln!("Bash::all_options_for_path:iter: sc={}", sc); + p = &find_subcmd!(p, sc).unwrap(); + } + + let opts = format!( + "{shorts} {longs} {pos} {subcmds}", + shorts = shorts!(p).fold(String::new(), |acc, s| format!("{} -{}", acc, s)), + // Handles aliases too + longs = longs!(p).fold(String::new(), |acc, l| format!( + "{} --{}", + acc, + l.to_str().unwrap() + )), + pos = positionals!(p).fold(String::new(), |acc, p| format!("{} {}", acc, p)), + // Handles aliases too + subcmds = sc_names!(p).fold(String::new(), |acc, s| format!("{} {}", acc, s)) + ); + + opts +} diff --git a/clap_generate/src/generators/shells/elvish.rs b/clap_generate/src/generators/shells/elvish.rs new file mode 100644 index 00000000..cbb1b51a --- /dev/null +++ b/clap_generate/src/generators/shells/elvish.rs @@ -0,0 +1,133 @@ +// Std +use std::io::Write; + +// Internal +use crate::Generator; +use crate::INTERNAL_ERROR_MSG; +use clap::*; + +/// Generate elvish completion file +pub struct Elvish; + +impl Generator for Elvish { + fn file_name(name: &str) -> String { + format!("{}.elv", name) + } + + fn generate(app: &App, buf: &mut dyn Write) { + let bin_name = app.get_bin_name().unwrap(); + + let mut names = vec![]; + let subcommands_cases = generate_inner(app, "", &mut names); + + let result = format!( + r#" +edit:completion:arg-completer[{bin_name}] = [@words]{{ + fn spaces [n]{{ + repeat $n ' ' | joins '' + }} + fn cand [text desc]{{ + edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc + }} + command = '{bin_name}' + for word $words[1:-1] {{ + if (has-prefix $word '-') {{ + break + }} + command = $command';'$word + }} + completions = [{subcommands_cases} + ] + $completions[$command] +}} +"#, + bin_name = bin_name, + subcommands_cases = subcommands_cases + ); + + w!(buf, result.as_bytes()); + } +} + +// Escape string inside single quotes +fn escape_string(string: &str) -> String { + string.replace("'", "''") +} + +fn get_tooltip(help: Option<&str>, data: T) -> String { + match help { + Some(help) => escape_string(help), + _ => data.to_string(), + } +} + +fn generate_inner<'b>( + p: &'b App<'b>, + previous_command_name: &str, + names: &mut Vec<&'b str>, +) -> String { + debugln!("Elvish::generate_inner;"); + + let command_name = if previous_command_name.is_empty() { + p.get_bin_name().expect(INTERNAL_ERROR_MSG).to_string() + } else { + format!("{};{}", previous_command_name, &p.name) + }; + + let mut completions = String::new(); + let preamble = String::from("\n cand "); + + for option in opts!(p) { + if let Some(data) = option.short { + let tooltip = get_tooltip(option.help, data); + + completions.push_str(&preamble); + completions.push_str(format!("-{} '{}'", data, tooltip).as_str()); + } + + if let Some(data) = option.long { + let tooltip = get_tooltip(option.help, data); + + completions.push_str(&preamble); + completions.push_str(format!("--{} '{}'", data, tooltip).as_str()); + } + } + + for flag in flags!(p) { + if let Some(data) = flag.short { + let tooltip = get_tooltip(flag.help, data); + + completions.push_str(&preamble); + completions.push_str(format!("-{} '{}'", data, tooltip).as_str()); + } + + if let Some(data) = flag.long { + let tooltip = get_tooltip(flag.help, data); + + completions.push_str(&preamble); + completions.push_str(format!("--{} '{}'", data, tooltip).as_str()); + } + } + + for subcommand in &p.subcommands { + let data = &subcommand.name; + let tooltip = get_tooltip(subcommand.about, data); + + completions.push_str(&preamble); + completions.push_str(format!("{} '{}'", data, tooltip).as_str()); + } + + let mut subcommands_cases = format!( + r" + &'{}'= {{{} + }}", + &command_name, completions + ); + + for subcommand in &p.subcommands { + let subcommand_subcommands_cases = generate_inner(&subcommand, &command_name, names); + subcommands_cases.push_str(&subcommand_subcommands_cases); + } + + subcommands_cases +} diff --git a/clap_generate/src/generators/shells/fish.rs b/clap_generate/src/generators/shells/fish.rs new file mode 100644 index 00000000..9503d0e0 --- /dev/null +++ b/clap_generate/src/generators/shells/fish.rs @@ -0,0 +1,113 @@ +// Std +use std::io::Write; + +// Internal +use crate::Generator; +use clap::*; + +/// Generate fish completion file +pub struct Fish; + +impl Generator for Fish { + fn file_name(name: &str) -> String { + format!("{}.fish", name) + } + + fn generate(app: &App, buf: &mut dyn Write) { + let command = app.get_bin_name().unwrap(); + let mut buffer = String::new(); + + gen_fish_inner(command, app, command, &mut buffer); + w!(buf, buffer.as_bytes()); + } +} + +// Escape string inside single quotes +fn escape_string(string: &str) -> String { + string.replace("\\", "\\\\").replace("'", "\\'") +} + +fn gen_fish_inner(root_command: &str, app: &App, subcommand: &str, buffer: &mut String) { + debugln!("Fish::gen_fish_inner;"); + // example : + // + // complete + // -c {command} + // -d "{description}" + // -s {short} + // -l {long} + // -a "{possible_arguments}" + // -r # if require parameter + // -f # don't use file completion + // -n "__fish_use_subcommand" # complete for command "myprog" + // -n "__fish_seen_subcommand_from subcmd1" # complete for command "myprog subcmd1" + + let mut basic_template = format!("complete -c {} -n ", root_command); + + if root_command == subcommand { + basic_template.push_str("\"__fish_use_subcommand\""); + } else { + basic_template.push_str(format!("\"__fish_seen_subcommand_from {}\"", subcommand).as_str()); + } + + for option in opts!(app) { + let mut template = basic_template.clone(); + + if let Some(data) = option.short { + template.push_str(format!(" -s {}", data).as_str()); + } + + if let Some(data) = option.long { + template.push_str(format!(" -l {}", data).as_str()); + } + + if let Some(data) = option.help { + template.push_str(format!(" -d '{}'", escape_string(data)).as_str()); + } + + if let Some(ref data) = option.possible_vals { + template.push_str(format!(" -r -f -a \"{}\"", data.join(" ")).as_str()); + } + + buffer.push_str(template.as_str()); + buffer.push_str("\n"); + } + + for flag in flags!(app) { + let mut template = basic_template.clone(); + + if let Some(data) = flag.short { + template.push_str(format!(" -s {}", data).as_str()); + } + + if let Some(data) = flag.long { + template.push_str(format!(" -l {}", data).as_str()); + } + + if let Some(data) = flag.help { + template.push_str(format!(" -d '{}'", escape_string(data)).as_str()); + } + + buffer.push_str(template.as_str()); + buffer.push_str("\n"); + } + + for subcommand in subcommands!(app) { + let mut template = basic_template.clone(); + + template.push_str(" -f"); + template.push_str(format!(" -a \"{}\"", &subcommand.name).as_str()); + + if let Some(data) = subcommand.about { + template.push_str(format!(" -d '{}'", escape_string(data)).as_str()) + } + + buffer.push_str(template.as_str()); + buffer.push_str("\n"); + } + + // generate options of subcommands + for subapp in &app.subcommands { + gen_fish_inner(root_command, subapp, &subapp.name, buffer); + } +} diff --git a/clap_generate/src/generators/shells/mod.rs b/clap_generate/src/generators/shells/mod.rs new file mode 100644 index 00000000..ef3f4e52 --- /dev/null +++ b/clap_generate/src/generators/shells/mod.rs @@ -0,0 +1,11 @@ +mod bash; +mod elvish; +mod fish; +mod powershell; +mod zsh; + +pub use bash::Bash; +pub use elvish::Elvish; +pub use fish::Fish; +pub use powershell::PowerShell; +pub use zsh::Zsh; diff --git a/clap_generate/src/generators/shells/powershell.rs b/clap_generate/src/generators/shells/powershell.rs new file mode 100644 index 00000000..d7a0123d --- /dev/null +++ b/clap_generate/src/generators/shells/powershell.rs @@ -0,0 +1,171 @@ +// Std +use std::io::Write; + +// Internal +use crate::Generator; +use crate::INTERNAL_ERROR_MSG; +use clap::*; + +/// Generate powershell completion file +pub struct PowerShell; + +impl Generator for PowerShell { + fn file_name(name: &str) -> String { + format!("_{}.ps1", name) + } + + fn generate(app: &App, buf: &mut dyn Write) { + let bin_name = app.get_bin_name().unwrap(); + + let mut names = vec![]; + let subcommands_cases = generate_inner(app, "", &mut names); + + let result = format!( + r#" +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName '{bin_name}' -ScriptBlock {{ + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + '{bin_name}' + for ($i = 1; $i -lt $commandElements.Count; $i++) {{ + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-')) {{ + break + }} + $element.Value + }}) -join ';' + + $completions = @(switch ($command) {{{subcommands_cases} + }}) + + $completions.Where{{ $_.CompletionText -like "$wordToComplete*" }} | + Sort-Object -Property ListItemText +}} +"#, + bin_name = bin_name, + subcommands_cases = subcommands_cases + ); + + w!(buf, result.as_bytes()); + } +} + +// Escape string inside single quotes +fn escape_string(string: &str) -> String { + string.replace("'", "''") +} + +fn get_tooltip(help: Option<&str>, data: T) -> String { + match help { + Some(help) => escape_string(&help), + _ => data.to_string(), + } +} + +fn generate_inner<'b>( + p: &'b App<'b>, + previous_command_name: &str, + names: &mut Vec<&'b str>, +) -> String { + debugln!("PowerShell::generate_inner;"); + + let command_name = if previous_command_name.is_empty() { + p.get_bin_name().expect(INTERNAL_ERROR_MSG).to_string() + } else { + format!("{};{}", previous_command_name, &p.name) + }; + + let mut completions = String::new(); + let preamble = String::from("\n [CompletionResult]::new("); + + for option in opts!(p) { + if let Some(data) = option.short { + let tooltip = get_tooltip(option.help, data); + + completions.push_str(&preamble); + completions.push_str( + format!( + "'-{}', '{}', {}, '{}')", + data, data, "[CompletionResultType]::ParameterName", tooltip + ) + .as_str(), + ); + } + + if let Some(data) = option.long { + let tooltip = get_tooltip(option.help, data); + + completions.push_str(&preamble); + completions.push_str( + format!( + "'--{}', '{}', {}, '{}')", + data, data, "[CompletionResultType]::ParameterName", tooltip + ) + .as_str(), + ); + } + } + + for flag in flags!(p) { + if let Some(data) = flag.short { + let tooltip = get_tooltip(flag.help, data); + + completions.push_str(&preamble); + completions.push_str( + format!( + "'-{}', '{}', {}, '{}')", + data, data, "[CompletionResultType]::ParameterName", tooltip + ) + .as_str(), + ); + } + + if let Some(data) = flag.long { + let tooltip = get_tooltip(flag.help, data); + + completions.push_str(&preamble); + completions.push_str( + format!( + "'--{}', '{}', {}, '{}')", + data, data, "[CompletionResultType]::ParameterName", tooltip + ) + .as_str(), + ); + } + } + + for subcommand in subcommands!(p) { + let data = &subcommand.name; + let tooltip = get_tooltip(subcommand.about, data); + + completions.push_str(&preamble); + completions.push_str( + format!( + "'{}', '{}', {}, '{}')", + data, data, "[CompletionResultType]::ParameterValue", tooltip + ) + .as_str(), + ); + } + + let mut subcommands_cases = format!( + r" + '{}' {{{} + break + }}", + &command_name, completions + ); + + for subcommand in &p.subcommands { + let subcommand_subcommands_cases = generate_inner(&subcommand, &command_name, names); + subcommands_cases.push_str(&subcommand_subcommands_cases); + } + + subcommands_cases +} diff --git a/clap_generate/src/generators/shells/zsh.rs b/clap_generate/src/generators/shells/zsh.rs new file mode 100644 index 00000000..4a92e438 --- /dev/null +++ b/clap_generate/src/generators/shells/zsh.rs @@ -0,0 +1,526 @@ +// Std +use std::io::Write; + +// Internal +use crate::Generator; +use crate::INTERNAL_ERROR_MSG; +use clap::*; + +/// Generate zsh completion file +pub struct Zsh; + +impl Generator for Zsh { + fn file_name(name: &str) -> String { + format!("_{}", name) + } + + fn generate(app: &App, buf: &mut dyn Write) { + 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} + +_{name} \"$@\"", + name = app.get_bin_name().unwrap(), + initial_args = get_args_of(app), + subcommands = get_subcommands_of(app), + subcommand_details = subcommand_details(app) + ) + .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 deliniated 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 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: &App) -> String { + debugln!("ZshGen::subcommand_details;"); + + let name = p.get_bin_name().unwrap(); + + // 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 = name.replace(" ", "__"), + bin_name = name, + subcommands_and_args = subcommands_of(p) + )]; + + // Next we start looping through all the children, grandchildren, etc. + let mut all_subcommands = Zsh::all_subcommands(p); + + all_subcommands.sort(); + all_subcommands.dedup(); + + for &(_, ref bin_name) in &all_subcommands { + debugln!("Zsh::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)) + )); + } + + 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: &App) -> String { + debugln!("Zsh::subcommands_of;"); + + let mut ret = vec![]; + + fn add_sc(sc: &App, n: &str, ret: &mut Vec) { + debugln!("Zsh::add_sc;"); + + let s = format!( + "\"{name}:{help}\" \\", + name = n, + help = sc + .about + .unwrap_or("") + .replace("[", "\\[") + .replace("]", "\\]") + ); + + if !s.is_empty() { + ret.push(s); + } + } + + // The subcommands + for sc in subcommands!(p) { + debugln!("Zsh::subcommands_of:iter: subcommand={}", sc.name); + + add_sc(sc, &sc.name, &mut ret); + + if let Some(ref v) = sc.aliases { + for alias in v.iter().filter(|&&(_, vis)| vis).map(|&(n, _)| n) { + add_sc(sc, alias, &mut ret); + } + } + } + + 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: &App) -> String { + debugln!("Zsh::get_subcommands_of;"); + debugln!( + "Zsh::get_subcommands_of: Has subcommands...{:?}", + p.has_subcommands() + ); + + if !p.has_subcommands() { + return String::new(); + } + + let sc_names = Zsh::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}) + words=($line[{pos}] \"${{words[@]}}\") + (( CURRENT += 1 )) + curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\" + case $line[{pos}] in + {subcommands} + esac + ;; +esac", + name = p.name, + name_hyphen = p.get_bin_name().unwrap().replace(" ", "-"), + subcommands = subcmds.join("\n"), + pos = positionals!(p).count() + 1 + ) +} + +fn parser_of<'b>(p: &'b App<'b>, mut sc: &str) -> &'b App<'b> { + debugln!("Zsh::parser_of: sc={}", sc); + + if sc == p.get_bin_name().unwrap_or(&String::new()) { + return p; + } + + sc = sc.split(" ").last().unwrap(); + find_subcmd!(p, sc).expect(INTERNAL_ERROR_MSG) +} + +// Writes out the args section, which ends up being the flags, opts and postionals, and a jump to +// another ZSH function if there are 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 +// ':: :_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(p: &App) -> String { + debugln!("Zsh::get_args_of;"); + + let mut ret = vec![String::from("_arguments \"${_arguments_options[@]}\" \\")]; + let opts = write_opts_of(p); + let flags = write_flags_of(p); + let positionals = write_positionals_of(p); + + let sc_or_a = if p.has_subcommands() { + format!( + "\":: :_{name}_commands\" \\", + name = p.bin_name.as_ref().unwrap().replace(" ", "__") + ) + } else { + String::new() + }; + + let sc = if p.has_subcommands() { + format!("\"*::: :->{name}\" \\", name = p.name) + } else { + String::new() + }; + + if !opts.is_empty() { + ret.push(opts); + } + + if !flags.is_empty() { + ret.push(flags); + } + + if !positionals.is_empty() { + ret.push(positionals); + } + + 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") +} + +// Escape help string inside single quotes and brackets +fn escape_help(string: &str) -> String { + string + .replace("\\", "\\\\") + .replace("'", "'\\''") + .replace("[", "\\[") + .replace("]", "\\]") +} + +// Escape value string inside single quotes and parentheses +fn escape_value(string: &str) -> String { + string + .replace("\\", "\\\\") + .replace("'", "'\\''") + .replace("(", "\\(") + .replace(")", "\\)") + .replace(" ", "\\ ") +} + +fn write_opts_of(p: &App) -> String { + debugln!("Zsh::write_opts_of;"); + + let mut ret = vec![]; + + for o in opts!(p) { + debugln!("Zsh::write_opts_of:iter: o={}", o.name); + + let help = o.help.map_or(String::new(), escape_help); + let mut conflicts = get_zsh_arg_conflicts!(p, o, INTERNAL_ERROR_MSG); + + conflicts = if conflicts.is_empty() { + String::new() + } else { + format!("({})", conflicts) + }; + + // @TODO @soundness should probably be either multiple occurrences or multiple values and + // not both + let multiple = if o.is_set(ArgSettings::MultipleOccurrences) + || o.is_set(ArgSettings::MultipleValues) + { + "*" + } else { + "" + }; + + let pv = if let Some(ref pv_vec) = o.possible_vals { + format!( + ": :({})", + pv_vec + .iter() + .map(|v| escape_value(*v)) + .collect::>() + .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 + ); + + debugln!("write_opts_of:iter: Wrote...{}", &*s); + ret.push(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 + ); + + debugln!("write_opts_of:iter: Wrote...{}", &*l); + ret.push(l); + } + } + + ret.join("\n") +} + +fn write_flags_of(p: &App) -> String { + debugln!("Zsh::write_flags_of;"); + + let mut ret = vec![]; + + for f in flags!(p) { + debugln!("Zsh::write_flags_of:iter: f={}", f.name); + + let help = f.help.map_or(String::new(), escape_help); + 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::MultipleOccurrences) { + "*" + } else { + "" + }; + + if let Some(short) = f.short { + let s = format!( + "'{conflicts}{multiple}-{arg}[{help}]' \\", + multiple = multiple, + conflicts = conflicts, + arg = short, + help = help + ); + + debugln!("Zsh::write_flags_of:iter: Wrote...{}", &*s); + + ret.push(s); + } + + if let Some(long) = f.long { + let l = format!( + "'{conflicts}{multiple}--{arg}[{help}]' \\", + conflicts = conflicts, + multiple = multiple, + arg = long, + help = help + ); + + debugln!("Zsh::write_flags_of:iter: Wrote...{}", &*l); + + ret.push(l); + } + } + + ret.join("\n") +} + +fn write_positionals_of(p: &App) -> String { + debugln!("Zsh::write_positionals_of;"); + + let mut ret = vec![]; + + for arg in positionals!(p) { + debugln!("Zsh::write_positionals_of:iter: arg={}", arg.name); + + let optional = if !arg.is_set(ArgSettings::Required) { + ":" + } else { + "" + }; + + let a = format!( + "'{optional}:{name}{help}:{action}' \\", + optional = optional, + name = arg.name, + help = arg + .help + .map_or("".to_owned(), |v| " -- ".to_owned() + v) + .replace("[", "\\[") + .replace("]", "\\]"), + action = arg + .possible_vals + .as_ref() + .map_or("_files".to_owned(), |values| { + format!( + "({})", + values + .iter() + .map(|v| escape_value(*v)) + .collect::>() + .join(" ") + ) + }) + ); + + debugln!("Zsh::write_positionals_of:iter: Wrote...{}", a); + + ret.push(a); + } + + ret.join("\n") +} diff --git a/clap_generate/src/lib.rs b/clap_generate/src/lib.rs new file mode 100644 index 00000000..b0b0419a --- /dev/null +++ b/clap_generate/src/lib.rs @@ -0,0 +1,188 @@ +// Copyright ⓒ 2015-2018 Kevin B. Knapp +// +// `clap_generate` is distributed under the terms of both the MIT license and the Apache License +// (Version 2.0). +// See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files in this repository +// for more information. + +//! Generates stuff for [`clap`](https://github.com/clap-rs/clap) based CLIs + +#![doc(html_root_url = "https://docs.rs/clap_generate/3.0.0-beta.1")] +#![deny( + missing_docs, + trivial_casts, + unused_import_braces, + unused_allocation, + trivial_numeric_casts +)] + +const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \ + report at https://github.com/clap-rs/clap/issues"; + +#[macro_use] +#[allow(missing_docs)] +mod macros; + +/// Contains some popular generators +pub mod generators; + +use std::ffi::OsString; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +#[doc(inline)] +pub use generators::Generator; + +/// Generate a file for a specified generator at compile time. +/// +/// **NOTE:** to generate the file at compile time you must use a `build.rs` "Build Script" +/// +/// # Examples +/// +/// The following example generates a bash completion script via a `build.rs` script. In this +/// simple example, we'll demo a very small application with only a single subcommand and two +/// args. Real applications could be many multiple levels deep in subcommands, and have tens or +/// potentially hundreds of arguments. +/// +/// First, it helps if we separate out our `App` definition into a separate file. Whether you +/// do this as a function, or bare App definition is a matter of personal preference. +/// +/// ``` +/// // src/cli.rs +/// +/// use clap::{App, Arg}; +/// +/// pub fn build_cli() -> App<'static> { +/// App::new("compl") +/// .about("Tests completions") +/// .arg(Arg::with_name("file") +/// .help("some input file")) +/// .subcommand(App::new("test") +/// .about("tests things") +/// .arg(Arg::with_name("case") +/// .long("case") +/// .takes_value(true) +/// .help("the case to test"))) +/// } +/// ``` +/// +/// In our regular code, we can simply call this `build_cli()` function, then call +/// `get_matches()`, or any of the other normal methods directly after. For example: +/// +/// ```ignore +/// // src/main.rs +/// +/// mod cli; +/// +/// fn main() { +/// let m = cli::build_cli().get_matches(); +/// +/// // normal logic continues... +/// } +/// ``` +/// +/// Next, we set up our `Cargo.toml` to use a `build.rs` build script. +/// +/// ```toml +/// # Cargo.toml +/// build = "build.rs" +/// +/// [build-dependencies] +/// clap = "*" +/// ``` +/// +/// Next, we place a `build.rs` in our project root. +/// +/// ```ignore +/// use clap_generate::{generate_to, generators::Bash}; +/// +/// include!("src/cli.rs"); +/// +/// fn main() { +/// let outdir = match env::var_os("OUT_DIR") { +/// None => return, +/// Some(outdir) => outdir, +/// }; +/// +/// let mut app = build_cli(); +/// generate_to(&mut app, // We need to specify what generator to use +/// "myapp", // We need to specify the bin name manually +/// outdir); // We need to specify where to write to +/// } +/// ``` +/// +/// Now, once we compile there will be a `{bin_name}.bash` file in the directory. +/// Assuming we compiled with debug mode, it would be somewhere similar to +/// `/target/debug/build/myapp-/out/myapp.bash`. +/// +/// **NOTE:** Please look at the individual [generators](./generators/index.html) +/// to see the name of the files generated. +pub fn generate_to(app: &mut clap::App, bin_name: S, out_dir: T) +where + G: Generator, + S: Into, + T: Into, +{ + use std::error::Error; + + let out_dir = PathBuf::from(out_dir.into()); + let file_name = G::file_name(app.get_bin_name().unwrap()); + + let mut file = match File::create(out_dir.join(file_name)) { + Err(why) => panic!("couldn't create completion file: {}", why.description()), + Ok(file) => file, + }; + + generate::(app, bin_name, &mut file) +} + +/// Generate a completions file for a specified shell at runtime. +/// +/// Until `cargo install` can install extra files like a completion script, this may be +/// used e.g. in a command that outputs the contents of the completion script, to be +/// redirected into a file by the user. +/// +/// # Examples +/// +/// Assuming a separate `cli.rs` like the [example above](./fn.generate_to.html), +/// we can let users generate a completion script using a command: +/// +/// ```ignore +/// // src/main.rs +/// +/// mod cli; +/// use std::io; +/// use clap_generate::{generate_to, generators::Bash}; +/// +/// fn main() { +/// let matches = cli::build_cli().get_matches(); +/// +/// if matches.is_present("generate-bash-completions") { +/// generate(&mut cli::build_cli(), "myapp", &mut io::stdout()); +/// } +/// +/// // normal logic continues... +/// } +/// +/// ``` +/// +/// Usage: +/// +/// ```shell +/// $ myapp generate-bash-completions > /usr/share/bash-completion/completions/myapp.bash +/// ``` +pub fn generate(app: &mut clap::App, bin_name: S, buf: &mut dyn Write) +where + G: Generator, + S: Into, +{ + app.bin_name = Some(bin_name.into()); + + if !app.is_set(clap::AppSettings::Built) { + app._build(); + app._build_bin_names(); + } + + G::generate(app, buf) +} diff --git a/clap_generate/src/macros.rs b/clap_generate/src/macros.rs new file mode 100644 index 00000000..748b258e --- /dev/null +++ b/clap_generate/src/macros.rs @@ -0,0 +1,87 @@ +macro_rules! w { + ($buf:expr, $to_w:expr) => { + match $buf.write_all($to_w) { + Ok(..) => (), + Err(..) => panic!("Failed to write to generated file"), + } + }; +} + +macro_rules! get_zsh_arg_conflicts { + ($app:expr, $arg:ident, $msg:ident) => { + if let Some(ref conf_vec) = $arg.blacklist { + let mut v = vec![]; + + for arg_name in conf_vec { + let arg = find!($app, arg_name).expect($msg); + + if let Some(s) = arg.short { + v.push(format!("-{}", s)); + } + + if let Some(l) = arg.long { + v.push(format!("--{}", l)); + } + } + + v.join(" ") + } else { + String::new() + } + }; +} + +#[cfg(feature = "debug")] +#[cfg_attr(feature = "debug", macro_use)] +#[cfg_attr(feature = "debug", allow(unused_macros))] +mod debug_macros { + macro_rules! debugln { + ($fmt:expr) => (println!(concat!("DEBUG:clap_generate:", $fmt))); + ($fmt:expr, $($arg:tt)*) => (println!(concat!("DEBUG:clap_generate:",$fmt), $($arg)*)); + } + macro_rules! sdebugln { + ($fmt:expr) => (println!($fmt)); + ($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*)); + } + macro_rules! debug { + ($fmt:expr) => (print!(concat!("DEBUG:clap_generate:", $fmt))); + ($fmt:expr, $($arg:tt)*) => (print!(concat!("DEBUG:clap_generate:",$fmt), $($arg)*)); + } + macro_rules! sdebug { + ($fmt:expr) => (print!($fmt)); + ($fmt:expr, $($arg:tt)*) => (print!($fmt, $($arg)*)); + } +} + +#[cfg(not(feature = "debug"))] +#[cfg_attr(not(feature = "debug"), macro_use)] +#[cfg_attr(not(feature = "debug"), allow(unused_macros))] +mod debug_macros { + macro_rules! debugln { + ($fmt:expr) => {}; + ($fmt:expr, $($arg:tt)*) => {}; + } + macro_rules! sdebugln { + ($fmt:expr) => {}; + ($fmt:expr, $($arg:tt)*) => {}; + } + macro_rules! debug { + ($fmt:expr) => {}; + ($fmt:expr, $($arg:tt)*) => {}; + } +} + +macro_rules! find { + ($app:expr, $name:expr, $what:ident) => { + $what!($app).find(|a| &a.name == $name) + }; + ($app:expr, $name:expr) => { + $app.args.args.iter().find(|a| { + if let Some(v) = a.index { + &v == $name + } else { + false + } + }) + }; +} diff --git a/clap_generate/tests/completions.rs b/clap_generate/tests/completions.rs new file mode 100644 index 00000000..21fb628f --- /dev/null +++ b/clap_generate/tests/completions.rs @@ -0,0 +1,858 @@ +use std::fmt; +use clap::{App, Arg}; +use clap_generate::{generate, generators::*}; + +#[derive(PartialEq, Eq)] +pub struct PrettyString<'a>(pub &'a str); + +impl<'a> fmt::Debug for PrettyString<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.0) + } +} + +macro_rules! assert_eq { + ($left:expr, $right:expr) => { + pretty_assertions::assert_eq!(PrettyString($left), PrettyString($right)); + } +} + +static BASH: &'static str = r#"_myapp() { + local i cur prev opts cmds + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="" + opts="" + + for i in ${COMP_WORDS[@]} + do + case "${i}" in + myapp) + cmd="myapp" + ;; + + help) + cmd+="__help" + ;; + test) + cmd+="__test" + ;; + *) + ;; + esac + done + + case "${cmd}" in + myapp) + opts=" -h -V --help --version test help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + + myapp__help) + opts=" -h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + myapp__test) + opts=" -h -V --case --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + --case) + COMPREPLY=($(compgen -f ${cur})) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + esac +} + +complete -F _myapp -o bashdefault -o default myapp +"#; + +static ZSH: &'static str = r#"#compdef myapp + +autoload -U is-at-least + +_myapp() { + 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 + _arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::file -- some input file:_files' \ +":: :_myapp_commands" \ +"*::: :->myapp" \ +&& ret=0 + case $state in + (myapp) + words=($line[2] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:myapp-command-$line[2]:" + case $line[2] in + (test) +_arguments "${_arguments_options[@]}" \ +'--case=[the case to test]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; + esac + ;; +esac +} + +(( $+functions[_myapp_commands] )) || +_myapp_commands() { + local commands; commands=( + "test:tests things" \ +"help:Prints this message or the help of the given subcommand(s)" \ + ) + _describe -t commands 'myapp commands' commands "$@" +} +(( $+functions[_myapp__help_commands] )) || +_myapp__help_commands() { + local commands; commands=( + + ) + _describe -t commands 'myapp help commands' commands "$@" +} +(( $+functions[_myapp__test_commands] )) || +_myapp__test_commands() { + local commands; commands=( + + ) + _describe -t commands 'myapp test commands' commands "$@" +} + +_myapp "$@""#; + +static FISH: &'static str = r#"complete -c myapp -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' +complete -c myapp -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' +complete -c myapp -n "__fish_use_subcommand" -f -a "test" -d 'tests things' +complete -c myapp -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)' +complete -c myapp -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' +complete -c myapp -n "__fish_seen_subcommand_from test" -s h -l help -d 'Prints help information' +complete -c myapp -n "__fish_seen_subcommand_from test" -s V -l version -d 'Prints version information' +complete -c myapp -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' +complete -c myapp -n "__fish_seen_subcommand_from help" -s V -l version -d 'Prints version information' +"#; + +static POWERSHELL: &'static str = r#" +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName 'my_app' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + 'my_app' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-')) { + break + } + $element.Value + }) -join ';' + + $completions = @(switch ($command) { + 'my_app' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('test', 'test', [CompletionResultType]::ParameterValue, 'tests things') + [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Prints this message or the help of the given subcommand(s)') + break + } + 'my_app;test' { + [CompletionResult]::new('--case', 'case', [CompletionResultType]::ParameterName, 'the case to test') + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + break + } + 'my_app;help' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + break + } + }) + + $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | + Sort-Object -Property ListItemText +} +"#; + +static ELVISH: &'static str = r#" +edit:completion:arg-completer[my_app] = [@words]{ + fn spaces [n]{ + repeat $n ' ' | joins '' + } + fn cand [text desc]{ + edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc + } + command = 'my_app' + for word $words[1:-1] { + if (has-prefix $word '-') { + break + } + command = $command';'$word + } + completions = [ + &'my_app'= { + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + cand test 'tests things' + cand help 'Prints this message or the help of the given subcommand(s)' + } + &'my_app;test'= { + cand --case 'the case to test' + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + } + &'my_app;help'= { + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + } + ] + $completions[$command] +} +"#; + +static ELVISH_SPECIAL_CMDS: &'static str = r#" +edit:completion:arg-completer[my_app] = [@words]{ + fn spaces [n]{ + repeat $n ' ' | joins '' + } + fn cand [text desc]{ + edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc + } + command = 'my_app' + for word $words[1:-1] { + if (has-prefix $word '-') { + break + } + command = $command';'$word + } + completions = [ + &'my_app'= { + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + cand test 'tests things' + cand some_cmd 'tests other things' + cand some-cmd-with-hypens 'some-cmd-with-hypens' + cand help 'Prints this message or the help of the given subcommand(s)' + } + &'my_app;test'= { + cand --case 'the case to test' + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + } + &'my_app;some_cmd'= { + cand --config 'the other case to test' + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + } + &'my_app;some-cmd-with-hypens'= { + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + } + &'my_app;help'= { + cand -h 'Prints help information' + cand --help 'Prints help information' + cand -V 'Prints version information' + cand --version 'Prints version information' + } + ] + $completions[$command] +} +"#; + +static POWERSHELL_SPECIAL_CMDS: &'static str = r#" +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName 'my_app' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + 'my_app' + for ($i = 1; $i -lt $commandElements.Count; $i++) { + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-')) { + break + } + $element.Value + }) -join ';' + + $completions = @(switch ($command) { + 'my_app' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('test', 'test', [CompletionResultType]::ParameterValue, 'tests things') + [CompletionResult]::new('some_cmd', 'some_cmd', [CompletionResultType]::ParameterValue, 'tests other things') + [CompletionResult]::new('some-cmd-with-hypens', 'some-cmd-with-hypens', [CompletionResultType]::ParameterValue, 'some-cmd-with-hypens') + [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Prints this message or the help of the given subcommand(s)') + break + } + 'my_app;test' { + [CompletionResult]::new('--case', 'case', [CompletionResultType]::ParameterName, 'the case to test') + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + break + } + 'my_app;some_cmd' { + [CompletionResult]::new('--config', 'config', [CompletionResultType]::ParameterName, 'the other case to test') + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + break + } + 'my_app;some-cmd-with-hypens' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + break + } + 'my_app;help' { + [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information') + [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information') + [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information') + break + } + }) + + $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | + Sort-Object -Property ListItemText +} +"#; + +static ZSH_SPECIAL_CMDS: &'static str = r#"#compdef my_app + +autoload -U is-at-least + +_my_app() { + 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 + _arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +'::file -- some input file:_files' \ +":: :_my_app_commands" \ +"*::: :->my_app" \ +&& ret=0 + case $state in + (my_app) + words=($line[2] "${words[@]}") + (( CURRENT += 1 )) + curcontext="${curcontext%:*:*}:my_app-command-$line[2]:" + case $line[2] in + (test) +_arguments "${_arguments_options[@]}" \ +'--case=[the case to test]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; +(some_cmd) +_arguments "${_arguments_options[@]}" \ +'--config=[the other case to test]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; +(some-cmd-with-hypens) +_arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; +(help) +_arguments "${_arguments_options[@]}" \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 +;; + esac + ;; +esac +} + +(( $+functions[_my_app_commands] )) || +_my_app_commands() { + local commands; commands=( + "test:tests things" \ +"some_cmd:tests other things" \ +"some-cmd-with-hypens:" \ +"help:Prints this message or the help of the given subcommand(s)" \ + ) + _describe -t commands 'my_app commands' commands "$@" +} +(( $+functions[_my_app__help_commands] )) || +_my_app__help_commands() { + local commands; commands=( + + ) + _describe -t commands 'my_app help commands' commands "$@" +} +(( $+functions[_my_app__some-cmd-with-hypens_commands] )) || +_my_app__some-cmd-with-hypens_commands() { + local commands; commands=( + + ) + _describe -t commands 'my_app some-cmd-with-hypens commands' commands "$@" +} +(( $+functions[_my_app__some_cmd_commands] )) || +_my_app__some_cmd_commands() { + local commands; commands=( + + ) + _describe -t commands 'my_app some_cmd commands' commands "$@" +} +(( $+functions[_my_app__test_commands] )) || +_my_app__test_commands() { + local commands; commands=( + + ) + _describe -t commands 'my_app test commands' commands "$@" +} + +_my_app "$@""#; + +static FISH_SPECIAL_CMDS: &'static str = r#"complete -c my_app -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' +complete -c my_app -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' +complete -c my_app -n "__fish_use_subcommand" -f -a "test" -d 'tests things' +complete -c my_app -n "__fish_use_subcommand" -f -a "some_cmd" -d 'tests other things' +complete -c my_app -n "__fish_use_subcommand" -f -a "some-cmd-with-hypens" +complete -c my_app -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)' +complete -c my_app -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' +complete -c my_app -n "__fish_seen_subcommand_from test" -s h -l help -d 'Prints help information' +complete -c my_app -n "__fish_seen_subcommand_from test" -s V -l version -d 'Prints version information' +complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -l config -d 'the other case to test' +complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -s h -l help -d 'Prints help information' +complete -c my_app -n "__fish_seen_subcommand_from some_cmd" -s V -l version -d 'Prints version information' +complete -c my_app -n "__fish_seen_subcommand_from some-cmd-with-hypens" -s h -l help -d 'Prints help information' +complete -c my_app -n "__fish_seen_subcommand_from some-cmd-with-hypens" -s V -l version -d 'Prints version information' +complete -c my_app -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' +complete -c my_app -n "__fish_seen_subcommand_from help" -s V -l version -d 'Prints version information' +"#; + +static BASH_SPECIAL_CMDS: &'static str = r#"_my_app() { + local i cur prev opts cmds + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="" + opts="" + + for i in ${COMP_WORDS[@]} + do + case "${i}" in + my_app) + cmd="my_app" + ;; + + help) + cmd+="__help" + ;; + some-cmd-with-hypens) + cmd+="__some__cmd__with__hypens" + ;; + some_cmd) + cmd+="__some_cmd" + ;; + test) + cmd+="__test" + ;; + *) + ;; + esac + done + + case "${cmd}" in + my_app) + opts=" -h -V --help --version test some_cmd some-cmd-with-hypens help" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + + my_app__help) + opts=" -h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + my_app__some__cmd__with__hypens) + opts=" -h -V --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + my_app__some_cmd) + opts=" -h -V --config --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + --config) + COMPREPLY=($(compgen -f ${cur})) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + my_app__test) + opts=" -h -V --case --help --version " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + case "${prev}" in + + --case) + COMPREPLY=($(compgen -f ${cur})) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + ;; + esac +} + +complete -F _my_app -o bashdefault -o default my_app +"#; + +static FISH_SPECIAL_HELP: &'static str = r#"complete -c my_app -n "__fish_use_subcommand" -l single-quotes -d 'Can be \'always\', \'auto\', or \'never\'' +complete -c my_app -n "__fish_use_subcommand" -l double-quotes -d 'Can be "always", "auto", or "never"' +complete -c my_app -n "__fish_use_subcommand" -l backticks -d 'For more information see `echo test`' +complete -c my_app -n "__fish_use_subcommand" -l backslash -d 'Avoid \'\\n\'' +complete -c my_app -n "__fish_use_subcommand" -l brackets -d 'List packages [filter]' +complete -c my_app -n "__fish_use_subcommand" -l expansions -d 'Execute the shell command with $SHELL' +complete -c my_app -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' +complete -c my_app -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' +"#; + +static ZSH_SPECIAL_HELP: &'static str = r#"#compdef my_app + +autoload -U is-at-least + +_my_app() { + 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 + _arguments "${_arguments_options[@]}" \ +'--single-quotes[Can be '\''always'\'', '\''auto'\'', or '\''never'\'']' \ +'--double-quotes[Can be "always", "auto", or "never"]' \ +'--backticks[For more information see `echo test`]' \ +'--backslash[Avoid '\''\\n'\'']' \ +'--brackets[List packages \[filter\]]' \ +'--expansions[Execute the shell command with $SHELL]' \ +'-h[Prints help information]' \ +'--help[Prints help information]' \ +'-V[Prints version information]' \ +'--version[Prints version information]' \ +&& ret=0 + +} + +(( $+functions[_my_app_commands] )) || +_my_app_commands() { + local commands; commands=( + + ) + _describe -t commands 'my_app commands' commands "$@" +} + +_my_app "$@""#; + +fn build_app() -> App<'static> { + build_app_with_name("myapp") +} + +fn build_app_with_name(s: &'static str) -> App<'static> { + App::new(s) + .about("Tests completions") + .arg(Arg::with_name("file").help("some input file")) + .subcommand( + App::new("test").about("tests things").arg( + Arg::with_name("case") + .long("case") + .takes_value(true) + .help("the case to test"), + ), + ) +} + +fn build_app_special_commands() -> App<'static> { + build_app_with_name("my_app") + .subcommand( + App::new("some_cmd").about("tests other things").arg( + Arg::with_name("config") + .long("--config") + .takes_value(true) + .help("the other case to test"), + ), + ) + .subcommand(App::new("some-cmd-with-hypens")) +} + +fn build_app_special_help() -> App<'static> { + App::new("my_app") + .arg( + Arg::with_name("single-quotes") + .long("single-quotes") + .help("Can be 'always', 'auto', or 'never'"), + ) + .arg( + Arg::with_name("double-quotes") + .long("double-quotes") + .help("Can be \"always\", \"auto\", or \"never\""), + ) + .arg( + Arg::with_name("backticks") + .long("backticks") + .help("For more information see `echo test`"), + ) + .arg( + Arg::with_name("backslash") + .long("backslash") + .help("Avoid '\\n'"), + ) + .arg( + Arg::with_name("brackets") + .long("brackets") + .help("List packages [filter]"), + ) + .arg( + Arg::with_name("expansions") + .long("expansions") + .help("Execute the shell command with $SHELL"), + ) +} + +fn common(app: &mut App, name: &str, fixture: &str) { + let mut buf = vec![]; + generate::(app, name, &mut buf); + let string = String::from_utf8(buf).unwrap(); + + assert_eq!(&string, fixture); +} + +#[test] +fn bash() { + let mut app = build_app(); + common::(&mut app, "myapp", BASH); +} + +#[test] +fn zsh() { + let mut app = build_app(); + common::(&mut app, "myapp", ZSH); +} + +#[test] +fn fish() { + let mut app = build_app(); + common::(&mut app, "myapp", FISH); +} + +#[test] +fn powershell() { + let mut app = build_app(); + common::(&mut app, "my_app", POWERSHELL); +} + +#[test] +fn elvish() { + let mut app = build_app(); + common::(&mut app, "my_app", ELVISH); +} + +#[test] +fn elvish_with_special_commands() { + let mut app = build_app_special_commands(); + common::(&mut app, "my_app", ELVISH_SPECIAL_CMDS); +} + +#[test] +fn powershell_with_special_commands() { + let mut app = build_app_special_commands(); + common::(&mut app, "my_app", POWERSHELL_SPECIAL_CMDS); +} + +#[test] +fn bash_with_special_commands() { + let mut app = build_app_special_commands(); + common::(&mut app, "my_app", BASH_SPECIAL_CMDS); +} + +#[test] +fn fish_with_special_commands() { + let mut app = build_app_special_commands(); + common::(&mut app, "my_app", FISH_SPECIAL_CMDS); +} + +#[test] +fn zsh_with_special_commands() { + let mut app = build_app_special_commands(); + common::(&mut app, "my_app", ZSH_SPECIAL_CMDS); +} + +#[test] +fn fish_with_special_help() { + let mut app = build_app_special_help(); + common::(&mut app, "my_app", FISH_SPECIAL_HELP); +} + +#[test] +fn zsh_with_special_help() { + let mut app = build_app_special_help(); + common::(&mut app, "my_app", ZSH_SPECIAL_HELP); +} diff --git a/clap_generate/tests/version-numbers.rs b/clap_generate/tests/version-numbers.rs new file mode 100644 index 00000000..1b8189b7 --- /dev/null +++ b/clap_generate/tests/version-numbers.rs @@ -0,0 +1,6 @@ +use version_sync::assert_html_root_url_updated; + +#[test] +fn test_html_root_url() { + assert_html_root_url_updated!("src/lib.rs"); +} diff --git a/src/lib.rs b/src/lib.rs index 300c292e..2286a22d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -475,8 +475,11 @@ mod output; mod parse; mod util; +#[doc(hidden)] +pub use mkeymap::KeyType; + const INTERNAL_ERROR_MSG: &str = "Fatal internal error. Please consider filing a bug \ - report at https://github.com/kbknapp/clap-rs/issues"; + report at https://github.com/clap-rs/clap/issues"; const INVALID_UTF8: &str = "unexpected invalid UTF-8 code point"; /// @TODO @release @docs diff --git a/src/macros.rs b/src/macros.rs index 7a1c735b..2fdff9a2 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -919,18 +919,18 @@ macro_rules! write_nspaces { }}; } +#[macro_export] +#[doc(hidden)] macro_rules! flags { ($app:expr, $how:ident) => {{ $app.args .args .$how() - .filter(|a| { - !a.settings.is_set(crate::build::ArgSettings::TakesValue) && a.index.is_none() - }) + .filter(|a| !a.settings.is_set($crate::ArgSettings::TakesValue) && a.index.is_none()) .filter(|a| !a.help_heading.is_some()) }}; ($app:expr) => { - flags!($app, iter) + $crate::flags!($app, iter) }; } @@ -941,14 +941,14 @@ macro_rules! flags_mut { }; } +#[macro_export] +#[doc(hidden)] macro_rules! opts { ($app:expr, $how:ident) => {{ $app.args .args .$how() - .filter(|a| { - a.settings.is_set(crate::build::ArgSettings::TakesValue) && a.index.is_none() - }) + .filter(|a| a.settings.is_set($crate::ArgSettings::TakesValue) && a.index.is_none()) .filter(|a| !a.help_heading.is_some()) }}; ($app:expr) => { @@ -963,6 +963,8 @@ macro_rules! opts_mut { }; } +#[macro_export] +#[doc(hidden)] macro_rules! positionals { ($app:expr) => { $app.args @@ -997,6 +999,8 @@ macro_rules! subcommands_cloned { }; } +#[macro_export] +#[doc(hidden)] macro_rules! subcommands { ($app:expr, $how:ident) => { $app.subcommands.$how() @@ -1028,6 +1032,8 @@ macro_rules! find_subcmd_cloned { }}; } +#[macro_export] +#[doc(hidden)] macro_rules! find_subcmd { ($app:expr, $sc:expr) => {{ subcommands!($app).find(|a| match_alias!(a, $sc, &*a.name)) @@ -1047,7 +1053,9 @@ macro_rules! longs { }}; } -macro_rules! _names { +#[macro_export] +#[doc(hidden)] +macro_rules! names { (@args $app:expr) => {{ $app.args.args.iter().map(|a| &*a.name) }}; @@ -1061,12 +1069,16 @@ macro_rules! _names { }}; } +#[macro_export] +#[doc(hidden)] macro_rules! sc_names { ($app:expr) => {{ - _names!(@sc $app) + names!(@sc $app) }}; } +#[macro_export] +#[doc(hidden)] macro_rules! match_alias { ($a:expr, $to:expr, $what:expr) => {{ $what == $to diff --git a/src/mkeymap.rs b/src/mkeymap.rs index f17cd225..d1e4097b 100644 --- a/src/mkeymap.rs +++ b/src/mkeymap.rs @@ -16,6 +16,7 @@ pub struct MKeyMap<'b> { built: bool, // mutation isn't possible after being built } +#[doc(hidden)] #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum KeyType { Short(char), From e6f77a8713f07c8cc72d8d087bdc6e925c16597b Mon Sep 17 00:00:00 2001 From: Pavan Kumar Sunkara Date: Thu, 6 Feb 2020 11:19:03 +0100 Subject: [PATCH 2/3] Added helper methods to generator --- clap_generate/examples/bash_completion.rs | 11 + clap_generate/src/generators/mod.rs | 346 ++++++++++++------ clap_generate/src/generators/shells/bash.rs | 48 ++- clap_generate/src/generators/shells/elvish.rs | 2 +- clap_generate/src/generators/shells/fish.rs | 20 +- .../src/generators/shells/powershell.rs | 2 +- clap_generate/src/generators/shells/zsh.rs | 6 +- clap_generate/src/lib.rs | 15 +- clap_generate/tests/completions.rs | 16 +- 9 files changed, 293 insertions(+), 173 deletions(-) create mode 100644 clap_generate/examples/bash_completion.rs diff --git a/clap_generate/examples/bash_completion.rs b/clap_generate/examples/bash_completion.rs new file mode 100644 index 00000000..d80d5848 --- /dev/null +++ b/clap_generate/examples/bash_completion.rs @@ -0,0 +1,11 @@ +use clap::App; +use clap_generate::{generate, generators::Bash}; +use std::io; + +fn main() { + let mut app = App::new("myapp") + .subcommand(App::new("test").subcommand(App::new("config"))) + .subcommand(App::new("hello")); + + generate::(&mut app, "myapp", &mut io::stdout()); +} diff --git a/clap_generate/src/generators/mod.rs b/clap_generate/src/generators/mod.rs index c04f09f2..3dd8fe18 100644 --- a/clap_generate/src/generators/mod.rs +++ b/clap_generate/src/generators/mod.rs @@ -5,7 +5,6 @@ use std::io::Write; // Internal use clap::*; - pub use shells::*; /// Generator trait which can be used to write generators @@ -55,39 +54,12 @@ pub trait Generator { /// ``` fn generate(app: &App, buf: &mut dyn Write); - /// Gets all subcommands including child subcommands in the form of 'name' where the name - /// is a single word (i.e. "install") of the path to said subcommand (i.e. - /// "rustup toolchain install") + /// Gets all subcommands including child subcommands in the form of `("name", "bin_name")`. /// - /// Also note, aliases are treated as their own subcommands but duplicates of whatever they're - /// aliasing. - fn all_subcommand_names(app: &App) -> Vec { - debugln!("all_subcommand_names;"); - - let mut subcmds: Vec<_> = Self::subcommands_of(app) - .iter() - .map(|&(ref n, _)| n.clone()) - .collect(); - - for sc_v in subcommands!(app).map(|s| Self::all_subcommand_names(&s)) { - subcmds.extend(sc_v); - } - - subcmds.sort(); - subcmds.dedup(); - subcmds - } - - /// Gets all subcommands including child subcommands in the form of ('name', 'bin_name') where the name - /// is a single word (i.e. "install") of the path and full bin_name of said subcommand (i.e. - /// "rustup toolchain install") - /// - /// Also note, aliases are treated as their own subcommands but duplicates of whatever they're - /// aliasing. + /// Subcommand `rustup toolchain install` would be converted to + /// `("install", "rustup toolchain install")`. fn all_subcommands(app: &App) -> Vec<(String, String)> { - debugln!("all_subcommands;"); - - let mut subcmds: Vec<_> = Self::subcommands_of(app); + let mut subcmds: Vec<_> = Self::subcommands(app); for sc_v in subcommands!(app).map(|s| Self::all_subcommands(&s)) { subcmds.extend(sc_v); @@ -96,120 +68,256 @@ pub trait Generator { subcmds } - /// Gets all subcommands exlcuding child subcommands in the form of (name, bin_name) where the name - /// is a single word (i.e. "install") and the bin_name is a space deliniated list of the path to said - /// subcommand (i.e. "rustup toolchain install") + /// Finds the subcommand [`clap::App`][clap] from the given [`clap::App`][clap] with the given path. /// - /// Also note, aliases are treated as their own subcommands but duplicates of whatever they're - /// aliasing. - fn subcommands_of(p: &App) -> Vec<(String, String)> { - debugln!( - "subcommands_of: name={}, bin_name={}", - p.name, - p.bin_name.as_ref().unwrap() - ); - debugln!( - "subcommands_of: Has subcommands...{:?}", - p.has_subcommands() - ); + /// **NOTE:** `path` should not contain the root `bin_name`. + /// + /// [clap]: ../clap/struct.App.html + fn find_subcommand_with_path<'b>(p: &'b App<'b>, path: Vec<&str>) -> &'b App<'b> { + let mut app = p; + + for sc in path { + app = find_subcmd!(app, sc).unwrap(); + } + + app + } + + /// Gets subcommands of [`clap::App`](../clap/struct.App.html) in the form of `("name", "bin_name")`. + /// + /// Subcommand `rustup toolchain install` would be converted to + /// `("install", "rustup toolchain install")`. + fn subcommands(p: &App) -> Vec<(String, String)> { + debugln!("subcommands: name={}", p.name); + debugln!("subcommands: Has subcommands...{:?}", p.has_subcommands()); let mut subcmds = vec![]; if !p.has_subcommands() { - let mut ret = vec![]; - - debugln!("subcommands_of: Looking for aliases..."); - - if let Some(ref aliases) = p.aliases { - for &(n, _) in aliases { - debugln!("subcommands_of:iter:iter: Found alias...{}", n); - - let mut als_bin_name: Vec<_> = - p.bin_name.as_ref().unwrap().split(' ').collect(); - - als_bin_name.push(n); - - let old = als_bin_name.len() - 2; - - als_bin_name.swap_remove(old); - ret.push((n.to_owned(), als_bin_name.join(" "))); - } - } - - return ret; + return subcmds; } - for sc in subcommands!(p) { + for sc in &p.subcommands { + let sc_bin_name = sc.get_bin_name().unwrap(); + debugln!( - "subcommands_of:iter: name={}, bin_name={}", + "subcommands:iter: name={}, bin_name={}", sc.name, - sc.bin_name.as_ref().unwrap() + sc_bin_name ); - debugln!("subcommands_of:iter: Looking for aliases..."); - if let Some(ref aliases) = sc.aliases { - for &(n, _) in aliases { - debugln!("subcommands_of:iter:iter: Found alias...{}", n); - - let mut als_bin_name: Vec<_> = - p.bin_name.as_ref().unwrap().split(' ').collect(); - - als_bin_name.push(n); - - let old = als_bin_name.len() - 2; - - als_bin_name.swap_remove(old); - subcmds.push((n.to_owned(), als_bin_name.join(" "))); - } - } - - subcmds.push((sc.name.clone(), sc.get_bin_name().unwrap().to_string())); + subcmds.push((sc.name.clone(), sc_bin_name.to_string())); } subcmds } - /// TODO - fn get_all_subcommand_paths(p: &App, first: bool) -> Vec { - debugln!("get_all_subcommand_paths;"); + /// Gets all the short options and flags of a [`clap::App`](../clap/struct.App.html). + /// Includes `h` and `V` depending on the [`clap::AppSettings`](../clap/enum.AppSettings.html). + fn shorts<'b>(p: &'b App<'b>) -> Vec { + debugln!("shorts: name={}", p.name); - let mut subcmds = vec![]; - - if !p.has_subcommands() { - if !first { - let name = &*p.name; - let path = p.get_bin_name().unwrap().to_string().replace(" ", "__"); - let mut ret = vec![path.clone()]; - - if let Some(ref aliases) = p.aliases { - for &(n, _) in aliases { - ret.push(path.replace(name, n)); - } + let mut shorts: Vec = p + .args + .args + .iter() + .filter_map(|a| { + if a.index.is_none() && a.short.is_some() { + Some(a.short.unwrap()) + } else { + None } + }) + .collect(); - return ret; - } - - return vec![]; + if shorts.iter().find(|x| **x == 'h').is_none() { + shorts.push('h'); } - for sc in subcommands!(p) { - let name = &*sc.name; - let path = sc.get_bin_name().unwrap().to_string().replace(" ", "__"); + if !p.is_set(AppSettings::DisableVersion) && shorts.iter().find(|x| **x == 'V').is_none() { + shorts.push('V'); + } - subcmds.push(path.clone()); + shorts + } - if let Some(ref aliases) = sc.aliases { - for &(n, _) in aliases { - subcmds.push(path.replace(name, n)); + /// Gets all the long options and flags of a [`clap::App`](../clap/struct.App.html). + /// Includes `help` and `version` depending on the [`clap::AppSettings`](../clap/enum.AppSettings.html). + fn longs<'b>(p: &'b App<'b>) -> Vec { + debugln!("longs: name={}", p.name); + + let mut longs: Vec = p + .args + .args + .iter() + .filter_map(|a| { + if a.index.is_none() && a.long.is_some() { + Some(a.long.unwrap().to_string()) + } else { + None } - } + }) + .collect(); + + if longs.iter().find(|x| **x == "help").is_none() { + longs.push(String::from("help")); } - for sc_v in subcommands!(p).map(|s| Self::get_all_subcommand_paths(&s, false)) { - subcmds.extend(sc_v); + if !p.is_set(AppSettings::DisableVersion) + && longs.iter().find(|x| **x == "version").is_none() + { + longs.push(String::from("version")); } - subcmds + longs + } + + /// Gets all the flags of a [`clap::App`](../clap/struct.App.html). + /// Includes `help` and `version` depending on the [`clap::AppSettings`](../clap/enum.AppSettings.html). + fn flags<'b>(p: &'b App<'b>) -> Vec { + debugln!("flags: name={}", p.name); + + let mut flags: Vec<_> = flags!(p).cloned().collect(); + + if flags.iter().find(|x| x.name == "help").is_none() { + flags.push( + Arg::with_name("help") + .short('h') + .long("help") + .help("Prints help information"), + ); + } + + if !p.is_set(AppSettings::DisableVersion) + && flags.iter().find(|x| x.name == "version").is_none() + { + flags.push( + Arg::with_name("version") + .short('V') + .long("version") + .help("Prints version information"), + ); + } + + flags + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + struct Foo; + + impl Generator for Foo { + fn generate(_: &App, _: &mut dyn Write) {} + + fn file_name(name: &str) -> String { + name.to_string() + } + } + + fn common() -> App<'static> { + let mut app = App::new("myapp") + .subcommand( + App::new("test") + .subcommand(App::new("config")) + .arg(Arg::with_name("file").short('f').long("file")), + ) + .subcommand(App::new("hello")) + .bin_name("my-app"); + + app._build(); + app._build_bin_names(); + app + } + + #[test] + fn test_subcommands() { + let app = common(); + + assert_eq!( + Foo::subcommands(&app), + vec![ + ("test".to_string(), "my-app test".to_string()), + ("hello".to_string(), "my-app hello".to_string()), + ("help".to_string(), "my-app help".to_string()), + ] + ); + } + + #[test] + fn test_all_subcommands() { + let app = common(); + + assert_eq!( + Foo::all_subcommands(&app), + vec![ + ("test".to_string(), "my-app test".to_string()), + ("hello".to_string(), "my-app hello".to_string()), + ("help".to_string(), "my-app help".to_string()), + ("config".to_string(), "my-app test config".to_string()), + ] + ); + } + + #[test] + fn test_find_subcommand_with_path() { + let app = common(); + let sc_app = Foo::find_subcommand_with_path(&app, "test config".split(" ").collect()); + + assert_eq!(sc_app.name, "config"); + } + + #[test] + fn test_flags() { + let app = common(); + let flags = Foo::flags(&app); + + assert_eq!(flags.len(), 2); + assert_eq!(flags[0].long, Some("help")); + assert_eq!(flags[1].long, Some("version")); + + let sc_flags = Foo::flags(Foo::find_subcommand_with_path(&app, vec!["test"])); + + assert_eq!(sc_flags.len(), 3); + assert_eq!(sc_flags[0].long, Some("file")); + assert_eq!(sc_flags[1].long, Some("help")); + assert_eq!(sc_flags[2].long, Some("version")); + } + + #[test] + fn test_shorts() { + let app = common(); + let shorts = Foo::shorts(&app); + + assert_eq!(shorts.len(), 2); + assert_eq!(shorts[0], 'h'); + assert_eq!(shorts[1], 'V'); + + let sc_shorts = Foo::shorts(Foo::find_subcommand_with_path(&app, vec!["test"])); + + assert_eq!(sc_shorts.len(), 3); + assert_eq!(sc_shorts[0], 'f'); + assert_eq!(sc_shorts[1], 'h'); + assert_eq!(sc_shorts[2], 'V'); + } + + #[test] + fn test_longs() { + let app = common(); + let longs = Foo::longs(&app); + + assert_eq!(longs.len(), 2); + assert_eq!(longs[0], "help"); + assert_eq!(longs[1], "version"); + + let sc_longs = Foo::longs(Foo::find_subcommand_with_path(&app, vec!["test"])); + + assert_eq!(sc_longs.len(), 3); + assert_eq!(sc_longs[0], "file"); + assert_eq!(sc_longs[1], "help"); + assert_eq!(sc_longs[2], "version"); } } diff --git a/clap_generate/src/generators/shells/bash.rs b/clap_generate/src/generators/shells/bash.rs index ef4efca8..f7ffef21 100644 --- a/clap_generate/src/generators/shells/bash.rs +++ b/clap_generate/src/generators/shells/bash.rs @@ -76,7 +76,13 @@ fn all_subcommands(app: &App) -> String { debugln!("Bash::all_subcommands;"); let mut subcmds = String::new(); - let scs = Bash::all_subcommand_names(app); + let mut scs = Bash::all_subcommands(app) + .iter() + .map(|x| x.0.clone()) + .collect::>(); + + scs.sort(); + scs.dedup(); for sc in &scs { subcmds = format!( @@ -97,10 +103,12 @@ fn subcommand_details(app: &App) -> String { debugln!("Bash::subcommand_details;"); let mut subcmd_dets = String::new(); - let mut scs = Bash::get_all_subcommand_paths(app, true); + let mut scs = Bash::all_subcommands(app) + .iter() + .map(|x| x.1.replace(" ", "__")) + .collect::>(); scs.sort(); - scs.dedup(); for sc in &scs { subcmd_dets = format!( @@ -123,7 +131,7 @@ fn subcommand_details(app: &App) -> String { subcmd_dets, subcmd = sc.replace("-", "__"), sc_opts = all_options_for_path(app, &*sc), - level = sc.split("__").map(|_| 1).fold(0, |acc, n| acc + n), + level = sc.split("__").map(|_| 1).sum::(), opts_details = option_details_for_path(app, &*sc) ); } @@ -134,13 +142,7 @@ fn subcommand_details(app: &App) -> String { fn option_details_for_path(app: &App, path: &str) -> String { debugln!("Bash::option_details_for_path: path={}", path); - let mut p = app; - - for sc in path.split("__").skip(1) { - debugln!("Bash::option_details_for_path:iter: sc={}", sc); - p = &find_subcmd!(p, sc).unwrap(); - } - + let p = Bash::find_subcommand_with_path(app, path.split("__").skip(1).collect()); let mut opts = String::new(); for o in opts!(p) { @@ -187,25 +189,19 @@ fn vals_for(o: &Arg) -> String { fn all_options_for_path(app: &App, path: &str) -> String { debugln!("Bash::all_options_for_path: path={}", path); - let mut p = app; - - for sc in path.split("__").skip(1) { - debugln!("Bash::all_options_for_path:iter: sc={}", sc); - p = &find_subcmd!(p, sc).unwrap(); - } + let p = Bash::find_subcommand_with_path(app, path.split("__").skip(1).collect()); + let scs: Vec<_> = Bash::subcommands(p).iter().map(|x| x.0.clone()).collect(); let opts = format!( "{shorts} {longs} {pos} {subcmds}", - shorts = shorts!(p).fold(String::new(), |acc, s| format!("{} -{}", acc, s)), - // Handles aliases too - longs = longs!(p).fold(String::new(), |acc, l| format!( - "{} --{}", - acc, - l.to_str().unwrap() - )), + shorts = Bash::shorts(p) + .iter() + .fold(String::new(), |acc, s| format!("{} -{}", acc, s)), + longs = Bash::longs(p) + .iter() + .fold(String::new(), |acc, l| format!("{} --{}", acc, l)), pos = positionals!(p).fold(String::new(), |acc, p| format!("{} {}", acc, p)), - // Handles aliases too - subcmds = sc_names!(p).fold(String::new(), |acc, s| format!("{} {}", acc, s)) + subcmds = scs.join(" "), ); opts diff --git a/clap_generate/src/generators/shells/elvish.rs b/clap_generate/src/generators/shells/elvish.rs index cbb1b51a..aa4bfa5c 100644 --- a/clap_generate/src/generators/shells/elvish.rs +++ b/clap_generate/src/generators/shells/elvish.rs @@ -93,7 +93,7 @@ fn generate_inner<'b>( } } - for flag in flags!(p) { + for flag in Elvish::flags(p) { if let Some(data) = flag.short { let tooltip = get_tooltip(flag.help, data); diff --git a/clap_generate/src/generators/shells/fish.rs b/clap_generate/src/generators/shells/fish.rs index 9503d0e0..966f08d0 100644 --- a/clap_generate/src/generators/shells/fish.rs +++ b/clap_generate/src/generators/shells/fish.rs @@ -17,7 +17,7 @@ impl Generator for Fish { let command = app.get_bin_name().unwrap(); let mut buffer = String::new(); - gen_fish_inner(command, app, command, &mut buffer); + gen_fish_inner(command, app, &mut buffer); w!(buf, buffer.as_bytes()); } } @@ -27,7 +27,7 @@ fn escape_string(string: &str) -> String { string.replace("\\", "\\\\").replace("'", "\\'") } -fn gen_fish_inner(root_command: &str, app: &App, subcommand: &str, buffer: &mut String) { +fn gen_fish_inner(root_command: &str, app: &App, buffer: &mut String) { debugln!("Fish::gen_fish_inner;"); // example : // @@ -43,13 +43,17 @@ fn gen_fish_inner(root_command: &str, app: &App, subcommand: &str, buffer: &mut // -n "__fish_seen_subcommand_from subcmd1" # complete for command "myprog subcmd1" let mut basic_template = format!("complete -c {} -n ", root_command); + let mut bin_name = app.get_bin_name().unwrap(); - if root_command == subcommand { + if root_command == bin_name { basic_template.push_str("\"__fish_use_subcommand\""); } else { - basic_template.push_str(format!("\"__fish_seen_subcommand_from {}\"", subcommand).as_str()); + bin_name = &app.name; + basic_template.push_str(format!("\"__fish_seen_subcommand_from {}\"", bin_name).as_str()); } + debugln!("Fish::gen_fish_inner; bin_name={}", bin_name); + for option in opts!(app) { let mut template = basic_template.clone(); @@ -73,7 +77,7 @@ fn gen_fish_inner(root_command: &str, app: &App, subcommand: &str, buffer: &mut buffer.push_str("\n"); } - for flag in flags!(app) { + for flag in Fish::flags(app) { let mut template = basic_template.clone(); if let Some(data) = flag.short { @@ -92,7 +96,7 @@ fn gen_fish_inner(root_command: &str, app: &App, subcommand: &str, buffer: &mut buffer.push_str("\n"); } - for subcommand in subcommands!(app) { + for subcommand in &app.subcommands { let mut template = basic_template.clone(); template.push_str(" -f"); @@ -107,7 +111,7 @@ fn gen_fish_inner(root_command: &str, app: &App, subcommand: &str, buffer: &mut } // generate options of subcommands - for subapp in &app.subcommands { - gen_fish_inner(root_command, subapp, &subapp.name, buffer); + for subcommand in &app.subcommands { + gen_fish_inner(root_command, subcommand, buffer); } } diff --git a/clap_generate/src/generators/shells/powershell.rs b/clap_generate/src/generators/shells/powershell.rs index d7a0123d..3d83c4a2 100644 --- a/clap_generate/src/generators/shells/powershell.rs +++ b/clap_generate/src/generators/shells/powershell.rs @@ -112,7 +112,7 @@ fn generate_inner<'b>( } } - for flag in flags!(p) { + for flag in PowerShell::flags(p) { if let Some(data) = flag.short { let tooltip = get_tooltip(flag.help, data); diff --git a/clap_generate/src/generators/shells/zsh.rs b/clap_generate/src/generators/shells/zsh.rs index 4a92e438..77e85c46 100644 --- a/clap_generate/src/generators/shells/zsh.rs +++ b/clap_generate/src/generators/shells/zsh.rs @@ -216,7 +216,7 @@ fn get_subcommands_of(p: &App) -> String { return String::new(); } - let sc_names = Zsh::subcommands_of(p); + let sc_names = Zsh::subcommands(p); let mut subcmds = vec![]; for &(ref name, ref bin_name) in &sc_names { @@ -262,7 +262,7 @@ fn parser_of<'b>(p: &'b App<'b>, mut sc: &str) -> &'b App<'b> { return p; } - sc = sc.split(" ").last().unwrap(); + sc = sc.split(' ').last().unwrap(); find_subcmd!(p, sc).expect(INTERNAL_ERROR_MSG) } @@ -429,7 +429,7 @@ fn write_flags_of(p: &App) -> String { let mut ret = vec![]; - for f in flags!(p) { + for f in Zsh::flags(p) { debugln!("Zsh::write_flags_of:iter: f={}", f.name); let help = f.help.map_or(String::new(), escape_help); diff --git a/clap_generate/src/lib.rs b/clap_generate/src/lib.rs index b0b0419a..bd6b4925 100644 --- a/clap_generate/src/lib.rs +++ b/clap_generate/src/lib.rs @@ -15,9 +15,10 @@ unused_allocation, trivial_numeric_casts )] +#![allow(clippy::needless_doctest_main)] -const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \ - report at https://github.com/clap-rs/clap/issues"; +const INTERNAL_ERROR_MSG: &str = "Fatal internal error. Please consider filing a bug \ + report at https://github.com/clap-rs/clap/issues"; #[macro_use] #[allow(missing_docs)] @@ -106,9 +107,9 @@ pub use generators::Generator; /// }; /// /// let mut app = build_cli(); -/// generate_to(&mut app, // We need to specify what generator to use -/// "myapp", // We need to specify the bin name manually -/// outdir); // We need to specify where to write to +/// generate_to::(&mut app, // We need to specify what generator to use +/// "myapp", // We need to specify the bin name manually +/// outdir); // We need to specify where to write to /// } /// ``` /// @@ -153,13 +154,13 @@ where /// /// mod cli; /// use std::io; -/// use clap_generate::{generate_to, generators::Bash}; +/// use clap_generate::{generate, generators::Bash}; /// /// fn main() { /// let matches = cli::build_cli().get_matches(); /// /// if matches.is_present("generate-bash-completions") { -/// generate(&mut cli::build_cli(), "myapp", &mut io::stdout()); +/// generate::(&mut cli::build_cli(), "myapp", &mut io::stdout()); /// } /// /// // normal logic continues... diff --git a/clap_generate/tests/completions.rs b/clap_generate/tests/completions.rs index 21fb628f..05b1e66e 100644 --- a/clap_generate/tests/completions.rs +++ b/clap_generate/tests/completions.rs @@ -1,20 +1,20 @@ -use std::fmt; use clap::{App, Arg}; use clap_generate::{generate, generators::*}; +use std::fmt; #[derive(PartialEq, Eq)] pub struct PrettyString<'a>(pub &'a str); impl<'a> fmt::Debug for PrettyString<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.0) - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.0) + } } macro_rules! assert_eq { ($left:expr, $right:expr) => { pretty_assertions::assert_eq!(PrettyString($left), PrettyString($right)); - } + }; } static BASH: &'static str = r#"_myapp() { @@ -45,7 +45,7 @@ static BASH: &'static str = r#"_myapp() { case "${cmd}" in myapp) - opts=" -h -V --help --version test help" + opts=" -h -V --help --version test help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 @@ -572,7 +572,7 @@ static BASH_SPECIAL_CMDS: &'static str = r#"_my_app() { case "${cmd}" in my_app) - opts=" -h -V --help --version test some_cmd some-cmd-with-hypens help" + opts=" -h -V --help --version test some_cmd some-cmd-with-hypens help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 @@ -740,7 +740,7 @@ fn build_app_special_commands() -> App<'static> { .help("the other case to test"), ), ) - .subcommand(App::new("some-cmd-with-hypens")) + .subcommand(App::new("some-cmd-with-hypens").alias("hyphen")) } fn build_app_special_help() -> App<'static> { From da7e9e55059e324f53349057f445a73ea8708edd Mon Sep 17 00:00:00 2001 From: Ole Martin Ruud Date: Wed, 2 Oct 2019 17:55:25 +0200 Subject: [PATCH 3/3] Escape colon in zsh completion help --- clap_generate/src/generators/shells/zsh.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clap_generate/src/generators/shells/zsh.rs b/clap_generate/src/generators/shells/zsh.rs index 77e85c46..3acdaab2 100644 --- a/clap_generate/src/generators/shells/zsh.rs +++ b/clap_generate/src/generators/shells/zsh.rs @@ -501,7 +501,8 @@ fn write_positionals_of(p: &App) -> String { .help .map_or("".to_owned(), |v| " -- ".to_owned() + v) .replace("[", "\\[") - .replace("]", "\\]"), + .replace("]", "\\]") + .replace(":", "\\:"), action = arg .possible_vals .as_ref()