From e75b6c7b75f729afb9eb1d2a2faf61dca7674634 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Tue, 28 Jun 2016 23:38:22 -0400 Subject: [PATCH 1/3] feat(Completions): one can now generate a bash completions script at compile time By using a build.rs "build script" one can now generate a bash completions script which allows tab completions for the entire program, to include, subcommands, options, everything! See the documentation for full examples and details. Closes #376 --- src/app/mod.rs | 10 +- src/app/parser.rs | 56 ++++++++-- src/completions.rs | 261 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 11 ++ 4 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 src/completions.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index 93b3b81a..02ef36fe 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2,7 +2,7 @@ mod settings; #[macro_use] mod macros; -mod parser; +pub mod parser; mod meta; mod help; @@ -27,6 +27,7 @@ use app::parser::Parser; use app::help::Help; use errors::Error; use errors::Result as ClapResult; +use shell::Shell; /// Used to create a representation of a command line program and all possible command line /// arguments. Application settings are set using the "builder pattern" with the @@ -939,6 +940,13 @@ impl<'a, 'b> App<'a, 'b> { self.p.write_version(w).map_err(From::from) } + + /// Generate a completions file for a specified shell + pub fn gen_completions, S: Into>(&mut self, bin_name: S, for_shell: Shell, out_dir: T) { + self.p.meta.bin_name = Some(bin_name.into()); + self.p.gen_completions(for_shell, out_dir.into()); + } + /// Starts the parsing process, upon a failed parse an error will be displayed to the user and /// the process will exit with the appropriate error code. By default this method gets all user /// provided arguments from [`env::args_os`] in order to allow for invalid UTF-8 code points, diff --git a/src/app/parser.rs b/src/app/parser.rs index d51017fc..29cdfbcf 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -24,6 +24,8 @@ use fmt::{Format, ColorWhen}; use osstringext::OsStrExt2; use app::meta::AppMeta; use args::MatchedArg; +use shell::Shell; +use completions::ComplGen; #[allow(missing_debug_implementations)] #[doc(hidden)] @@ -31,15 +33,15 @@ pub struct Parser<'a, 'b> where 'a: 'b { required: Vec<&'b str>, - short_list: Vec, - long_list: Vec<&'b str>, + pub short_list: Vec, + pub long_list: Vec<&'b str>, blacklist: Vec<&'b str>, // A list of possible flags flags: Vec>, // A list of possible options - opts: Vec>, + pub opts: Vec>, // A list of positional arguments - positionals: VecMap>, + pub positionals: VecMap>, // A list of subcommands #[doc(hidden)] pub subcommands: Vec>, @@ -97,6 +99,12 @@ impl<'a, 'b> Parser<'a, 'b> .nth(0); } + pub fn gen_completions(&mut self, for_shell: Shell, od: OsString) { + self.propogate_help_version(); + self.build_bin_names(); + ComplGen::new(self, od).generate(for_shell) + } + // actually adds the arguments pub fn add_arg(&mut self, a: &Arg<'a, 'b>) { debug_assert!(!(self.flags.iter().any(|f| &f.name == &a.name) || @@ -236,6 +244,7 @@ impl<'a, 'b> Parser<'a, 'b> self.required.iter() } + #[cfg_attr(feature = "lints", allow(for_kv_map))] pub fn get_required_from(&self, reqs: &[&'a str], @@ -652,7 +661,7 @@ impl<'a, 'b> Parser<'a, 'b> self.meta .bin_name .as_ref() - .unwrap_or(&String::new()), + .unwrap_or(&self.meta.name.clone()), if self.meta.bin_name.is_some() { " " } else { @@ -788,6 +797,41 @@ impl<'a, 'b> Parser<'a, 'b> Ok(()) } + fn propogate_help_version(&mut self) { + debugln!("exec=propogate_help_version;"); + self.create_help_and_version(); + for sc in self.subcommands.iter_mut() { + sc.p.propogate_help_version(); + } + } + + fn build_bin_names(&mut self) { + debugln!("exec=build_bin_names;"); + for sc in self.subcommands.iter_mut() { + debug!("bin_name set..."); + if sc.p.meta.bin_name.is_none() { + sdebugln!("No"); + let bin_name = format!("{}{}{}", + self.meta + .bin_name + .as_ref() + .unwrap_or(&self.meta.name.clone()), + if self.meta.bin_name.is_some() { + " " + } else { + "" + }, + &*sc.p.meta.name); + debugln!("Setting bin_name of {} to {}", self.meta.name, bin_name); + sc.p.meta.bin_name = Some(bin_name); + } else { + sdebugln!("yes ({:?})", sc.p.meta.bin_name); + } + debugln!("Calling build_bin_names from...{}", sc.p.meta.name); + sc.p.build_bin_names(); + } + } + fn parse_subcommand(&mut self, sc_name: String, matcher: &mut ArgMatcher<'a>, @@ -1258,7 +1302,7 @@ impl<'a, 'b> Parser<'a, 'b> // If there was a delimiter used, we're not looking for more values if val.contains_byte(delim as u32 as u8) || arg.is_set(ArgSettings::RequireDelimiter) { ret = None; - } + } } } else { ret = try!(self.add_single_val_to_arg(arg, val, matcher)); diff --git a/src/completions.rs b/src/completions.rs new file mode 100644 index 00000000..811fcea7 --- /dev/null +++ b/src/completions.rs @@ -0,0 +1,261 @@ +use std::path::PathBuf; +use std::fs::File; +use std::ffi::OsString; +use std::io::Write; + +use app::parser::Parser; +use shell::Shell; +use args::{ArgSettings, OptBuilder}; + +macro_rules! w { + ($_self:ident, $f:ident, $to_w:expr) => { + match $f.write_all($to_w) { + Ok(..) => (), + Err(..) => panic!(format!("Failed to write to file completions file")), + } + }; +} + +pub struct ComplGen<'a, 'b> where 'a: 'b { + p: &'b Parser<'a, 'b>, + out_dir: OsString, +} + +impl<'a, 'b> ComplGen<'a, 'b> { + pub fn new(p: &'b Parser<'a, 'b>, od: OsString) -> Self { + ComplGen { + p: p, + out_dir: od, + } + } + + pub fn generate(&self, for_shell: Shell) { + match for_shell { + Shell::Bash => self.gen_bash(), + } + } + + fn gen_bash(&self) { + use std::error::Error; + let out_dir = PathBuf::from(&self.out_dir); + + let mut file = match File::create(out_dir.join("bash.sh")) { + Err(why) => panic!("couldn't create bash completion file: {}", + why.description()), + Ok(file) => file, + }; + w!(self, file, 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 + ;; + {subcmd_details} + esac +}} + +complete -F _{name} {name} +", + name=self.p.meta.bin_name.as_ref().unwrap(), + name_opts=self.all_options_for_path(self.p.meta.bin_name.as_ref().unwrap()), + name_opts_details=self.option_details_for_path(self.p.meta.bin_name.as_ref().unwrap()), + subcmds=self.all_subcommands(), + subcmd_details=self.subcommand_details() + ).as_bytes()); + } + + fn all_subcommands(&self) -> String { + let mut subcmds = String::new(); + let mut scs = get_all_subcommands(self.p); + scs.sort(); + scs.dedup(); + + for sc in &scs { + subcmds = format!( + "{} + {name}) + cmd+=\"_{name}\" + ;;", + subcmds, + name=sc.replace("-", "_")); + } + + subcmds + } + + fn subcommand_details(&self) -> String { + let mut subcmd_dets = String::new(); + let mut scs = get_all_subcommand_paths(self.p, 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 + ;;", + subcmd_dets, + subcmd=sc.replace("-", "_"), + sc_opts=self.all_options_for_path(&*sc), + level=sc.split("_").map(|_|1).fold(0, |acc, n| acc + n), + opts_details=self.option_details_for_path(&*sc) + ); + } + + subcmd_dets + } + + fn all_options_for_path(&self, path: &str) -> String { + let mut p = self.p; + for sc in path.split("_").skip(1) { + debugln!("iter;sc={}", sc); + p = &p.subcommands.iter().filter(|s| s.p.meta.name == sc).next().unwrap().p; + } + let mut opts = p.short_list.iter().fold(String::new(), |acc, s| format!("{} -{}", acc, s)); + opts = format!("{} {}", opts, p.long_list.iter() + .fold(String::new(), |acc, l| format!("{} --{}", acc, l))); + opts = format!("{} {}", opts, p.positionals.values() + .fold(String::new(), |acc, p| format!("{} {}", acc, p))); + opts = format!("{} {}", opts, p.subcommands.iter() + .fold(String::new(), |acc, s| format!("{} {}", acc, s.p.meta.name))); + opts + } + + fn option_details_for_path(&self, path: &str) -> String { + let mut p = self.p; + for sc in path.split("_").skip(1) { + debugln!("iter;sc={}", sc); + p = &p.subcommands.iter().filter(|s| s.p.meta.name == sc).next().unwrap().p; + } + let mut opts = String::new(); + for o in &p.opts { + if let Some(l) = o.long { + opts = format!("{} + --{}) + COMPREPLY=(\"{}\") + ;;", opts, l, vals_for(o)); + } + if let Some(s) = o.short { + opts = format!("{} + -{}) + COMPREPLY=(\"{}\") + ;;", opts, s, vals_for(o)); + } + } + opts + } +} + +pub fn get_all_subcommands(p: &Parser) -> Vec { + let mut subcmds = vec![]; + if !p.has_subcommands() { + return vec![p.meta.name.clone()] + } + for sc in p.subcommands.iter().map(|ref s| s.p.meta.name.clone()) { + subcmds.push(sc); + } + for sc_v in p.subcommands.iter().map(|ref s| get_all_subcommands(&s.p)) { + subcmds.extend(sc_v); + } + subcmds +} + +pub fn get_all_subcommand_paths(p: &Parser, first: bool) -> Vec { + let mut subcmds = vec![]; + if !p.has_subcommands() { + if !first { + return vec![p.meta.bin_name.as_ref().unwrap().clone().replace(" ", "_")] + } + return vec![]; + } + for sc in p.subcommands.iter() + .map(|ref s| s.p.meta.bin_name.as_ref() + .unwrap() + .clone() + .replace(" ", "_")) { + subcmds.push(sc); + } + for sc_v in p.subcommands.iter().map(|ref s| get_all_subcommand_paths(&s.p, false)) { + subcmds.extend(sc_v); + } + subcmds +} + +fn vals_for(o: &OptBuilder) -> String { + use args::AnyArg; + let mut ret = String::new(); + if let Some(ref vec) = o.val_names() { + let mut it = vec.iter().peekable(); + while let Some((_, val)) = it.next() { + ret = format!("{}<{}>{}", ret, val, + if it.peek().is_some() { + " " + } else { + "" + }); + } + let num = vec.len(); + if o.is_set(ArgSettings::Multiple) && num == 1 { + ret = format!("{}...", ret); + } + } else if let Some(num) = o.num_vals() { + let mut it = (0..num).peekable(); + while let Some(_) = it.next() { + ret = format!("{}<{}>{}", ret, o.name(), + if it.peek().is_some() { + " " + } else { + "" + }); + } + if o.is_set(ArgSettings::Multiple) && num == 1 { + ret = format!("{}...", ret); + } + } else { + ret = format!("<{}>", o.name()); + if o.is_set(ArgSettings::Multiple) { + ret = format!("{}...", ret); + } + } + ret +} diff --git a/src/lib.rs b/src/lib.rs index 22074ad9..e975c12d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -423,6 +423,7 @@ pub use args::{Arg, ArgGroup, ArgMatches, ArgSettings, SubCommand, Values, OsVal pub use app::{App, AppSettings}; pub use fmt::Format; pub use errors::{Error, ErrorKind, Result}; +pub use shell::Shell; #[macro_use] mod macros; @@ -434,7 +435,17 @@ mod suggestions; mod errors; mod osstringext; mod strext; +mod completions; const INTERNAL_ERROR_MSG: &'static str = "Fatal internal error. Please consider filing a bug \ report at https://github.com/kbknapp/clap-rs/issues"; const INVALID_UTF8: &'static str = "unexpected invalid UTF-8 code point"; + +mod shell { + /// Describes which shell to produce a completions file for + #[derive(Debug, Copy, Clone)] + pub enum Shell { + /// Generates a .sh completion file for the Bourne Again SHell (BASH) + Bash + } +} From c6c519e40efd6c4533a9ef5efe8e74fd150391b7 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 1 Jul 2016 00:18:52 -0400 Subject: [PATCH 2/3] docs(Completions): adds documentation for completion scripts --- src/app/mod.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 02ef36fe..e4e278d3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -941,7 +941,81 @@ impl<'a, 'b> App<'a, 'b> { } - /// Generate a completions file for a specified shell + /// Generate a completions file for a specified shell at compile time. + /// + /// **NOTE:** to generate the this 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 + /// potentiall hundreds of arguments. + /// + /// First, it helps if we separate out our `App` definition into a seperate file. Whether you + /// do this as a function, or bare App definition is a matter of personal preference. + /// + /// ```no_run + /// // src/cli.rs + /// + /// use clap::{App, Arg, SubCommand}; + /// + /// fn build_cli() -> App<'static, 'static> { + /// App::new("compl") + /// .about("Tests completions") + /// .arg(Arg::with_name("file") + /// .help("some input file")) + /// .subcommand(SubCommand::with_name("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_mathces()`, or any of the other normal methods directly after. For example: + /// + /// ```no_run + /// src/main.rs + /// + /// use 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. + /// ```ignore + /// # Cargo.toml + /// build = "build.rs" + /// + /// [build-dependencies] + /// clap = "2.9" + /// ``` + /// + /// Next, we place a `build.rs` in our project root. + /// + /// ```no_run + /// extern crate clap; + /// + /// use clap::Shell; + /// + /// include!("src/cli.rs"); + /// + /// fn main() { + /// let mut app = build_cli(); + /// app.gen_completions("myapp", // We need to specify the bin name manually + /// Shell::Bash, // Then say which shell to build completions for + /// env!("OUT_DIR")); // Then say where write the completions to + /// } + /// ``` + /// Now, once we combile there will be a `bash.sh` file in the directory. Assuming we compiled + /// with debug mode, it would be somewhere similar to + /// `/target/debug/build/myapp-/out/bash.sh` pub fn gen_completions, S: Into>(&mut self, bin_name: S, for_shell: Shell, out_dir: T) { self.p.meta.bin_name = Some(bin_name.into()); self.p.gen_completions(for_shell, out_dir.into()); From 7daee9ded0d6343d1eed0aae0d8fa942883d7233 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 1 Jul 2016 00:25:01 -0400 Subject: [PATCH 3/3] chore: increase version --- CHANGELOG.md | 13 +++++++++++++ Cargo.toml | 2 +- README.md | 40 ++++++++-------------------------------- src/app/mod.rs | 6 +++--- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad89c112..3210eef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + +## v2.9.0 (2016-07-01) + + +#### Documentation + +* **Completions:** adds documentation for completion scripts ([c6c519e4](https://github.com/kbknapp/clap-rs/commit/c6c519e40efd6c4533a9ef5efe8e74fd150391b7)) + +#### Features + +* **Completions:** one can now generate a bash completions script at compile time! ([e75b6c7b](https://github.com/kbknapp/clap-rs/commit/e75b6c7b75f729afb9eb1d2a2faf61dca7674634), closes [#376](https://github.com/kbknapp/clap-rs/issues/376)) + + ## v2.8.0 (2016-06-30) diff --git a/Cargo.toml b/Cargo.toml index e1a0b980..f0256a03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "clap" -version = "2.8.0" +version = "2.9.0" authors = ["Kevin K. "] exclude = ["examples/*", "clap-test/*", "tests/*", "benches/*", "*.png", "clap-perf/*", "*.dot"] description = "A simple to use, efficient, and full featured Command Line Argument Parser" diff --git a/README.md b/README.md index c923fe31..4bca128b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# clap +clap +==== [![Crates.io](https://img.shields.io/crates/v/clap.svg)](https://crates.io/crates/clap) [![Crates.io](https://img.shields.io/crates/d/clap.svg)](https://crates.io/crates/clap) [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kbknapp/clap-rs/blob/master/LICENSE-MIT) [![Coverage Status](https://coveralls.io/repos/kbknapp/clap-rs/badge.svg?branch=master&service=github)](https://coveralls.io/github/kbknapp/clap-rs?branch=master) [![Join the chat at https://gitter.im/kbknapp/clap-rs](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kbknapp/clap-rs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -38,6 +39,10 @@ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) ## What's New +Here's the highlights for v2.9.0 + +* **Completions:** one can now generate a bash completions script at compile time! + Here's the highlights for v2.8.0 * **Arg:** adds new optional setting `Arg::require_delimiter` which requires val delimiter to parse multiple values @@ -192,6 +197,8 @@ Below are a few of the features which `clap` supports, full descriptions and usa * **Auto-generated Help, Version, and Usage information** - Can optionally be fully, or partially overridden if you want a custom help, version, or usage +* **Auto-generated bash completion scripts at compile time** + - Even works through many multiple levels of subcommands * **Flags / Switches** (i.e. bool fields) - Both short and long versions supported (i.e. `-f` and `--flag` respectively) - Supports combining short versions (i.e. `-fBgoZ` is the same as `-f -B -g -o -Z`) @@ -340,37 +347,6 @@ fn main() { } ``` -The following combines the previous two examples by using the less verbose `from_usage` methods and the performance of the Builder Pattern. - -```rust -// (Full example with detailed comments in examples/01c_quick_example.rs) -// Must be compiled with `--features unstable` -// -// This example demonstrates clap's "usage strings" method of creating arguments which is less -// less verbose -#[macro_use] -extern crate clap; - -fn main() { - let matches = clap_app!(myapp => - (version: "1.0") - (author: "Kevin K. ") - (about: "Does awesome things") - (@arg config: -c --config +takes_value "Sets a custom config file") - (@arg INPUT: +required "Sets the input file to use") - (@arg verbose: -v ... "Sets the level of verbosity") - (@subcommand test => - (about: "controls testing features") - (version: "1.3") - (author: "Someone E. ") - (@arg verbose: -d --debug "Print debug information") - ) - ).get_matches(); - -// Same as previous examples... -} -``` - This final method shows how you can use a YAML file to build your CLI and keep your Rust source tidy or support multiple localized translations by having different YAML files for each localization. First, create the `cli.yml` file to hold your CLI options, but it could be called anything we like (we'll use the same both examples above to keep it functionally equivalent): ```yaml diff --git a/src/app/mod.rs b/src/app/mod.rs index e4e278d3..e70d7807 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -955,7 +955,7 @@ impl<'a, 'b> App<'a, 'b> { /// First, it helps if we separate out our `App` definition into a seperate file. Whether you /// do this as a function, or bare App definition is a matter of personal preference. /// - /// ```no_run + /// ```ignore /// // src/cli.rs /// /// use clap::{App, Arg, SubCommand}; @@ -977,7 +977,7 @@ impl<'a, 'b> App<'a, 'b> { /// In our regular code, we can simply call this `build_cli()` function, then call /// `get_mathces()`, or any of the other normal methods directly after. For example: /// - /// ```no_run + /// ```ignore /// src/main.rs /// /// use cli; @@ -999,7 +999,7 @@ impl<'a, 'b> App<'a, 'b> { /// /// Next, we place a `build.rs` in our project root. /// - /// ```no_run + /// ```ignore /// extern crate clap; /// /// use clap::Shell;