From 9f62cf7378ba5acb5ce8c5bac89b4aa60c30755f Mon Sep 17 00:00:00 2001 From: David Szotten Date: Tue, 5 Jul 2016 09:53:28 +0100 Subject: [PATCH] feat(Completions): adds the ability to generate completions to io::Write object --- src/app/mod.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/app/parser.rs | 21 +++++++++++++++++++-- src/completions.rs | 27 +++++++-------------------- tests/completions.rs | 25 +++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 tests/completions.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index 1ff2ff5a5bf..56011ca1629 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -978,7 +978,7 @@ impl<'a, 'b> App<'a, 'b> { /// `get_matches()`, or any of the other normal methods directly after. For example: /// /// ```ignore - /// src/main.rs + /// // src/main.rs /// /// mod cli; /// @@ -1023,6 +1023,44 @@ impl<'a, 'b> App<'a, 'b> { self.p.gen_completions(for_shell, out_dir.into()); } + + /// 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](./struct.App.html#method.gen_completions), + /// we can let users generate a completion script using a command: + /// + /// ```ignore + /// // src/main.rs + /// + /// mod cli; + /// use std::io; + /// + /// fn main() { + /// let matches = cli::build_cli().get_matches(); + /// + /// if matches.is_present("generate-bash-completions") { + /// cli::build_cli().gen_completions_to("myapp", Shell::Bash, &mut io::stdout()); + /// } + /// + /// // normal logic continues... + /// } + /// + /// ``` + /// + /// Usage: + /// + /// ```shell + /// $ myapp generate-bash-completions > /etc/bash_completion.d/myapp + /// ``` + pub fn gen_completions_to>(&mut self, bin_name: S, for_shell: Shell, buf: &mut W) { + self.p.meta.bin_name = Some(bin_name.into()); + self.p.gen_completions_to(for_shell, buf); + } + /// 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 174109f0114..4f7f872d0e4 100644 --- a/src/app/parser.rs +++ b/src/app/parser.rs @@ -26,6 +26,8 @@ use app::meta::AppMeta; use args::MatchedArg; use shell::Shell; use completions::ComplGen; +use std::fs::File; +use std::path::PathBuf; #[allow(missing_debug_implementations)] #[doc(hidden)] @@ -99,10 +101,25 @@ impl<'a, 'b> Parser<'a, 'b> .nth(0); } - pub fn gen_completions(&mut self, for_shell: Shell, od: OsString) { + pub fn gen_completions_to(&mut self, for_shell: Shell, buf: &mut W) { + self.propogate_help_version(); self.build_bin_names(); - ComplGen::new(self, od).generate(for_shell) + + ComplGen::new(self).generate(for_shell, buf) + } + + pub fn gen_completions(&mut self, for_shell: Shell, od: OsString) { + use std::error::Error; + + let out_dir = PathBuf::from(od); + + let mut file = match File::create(out_dir.join(format!("{}_bash.sh", &*self.meta.bin_name.as_ref().unwrap()))) { + Err(why) => panic!("couldn't create bash completion file: {}", + why.description()), + Ok(file) => file, + }; + self.gen_completions_to(for_shell, &mut file) } // actually adds the arguments diff --git a/src/completions.rs b/src/completions.rs index 3f693d1fea2..0d7c0b62b4c 100644 --- a/src/completions.rs +++ b/src/completions.rs @@ -1,6 +1,3 @@ -use std::path::PathBuf; -use std::fs::File; -use std::ffi::OsString; use std::io::Write; use app::parser::Parser; @@ -8,8 +5,8 @@ use shell::Shell; use args::{ArgSettings, OptBuilder}; macro_rules! w { - ($_self:ident, $f:ident, $to_w:expr) => { - match $f.write_all($to_w) { + ($buf:expr, $to_w:expr) => { + match $buf.write_all($to_w) { Ok(..) => (), Err(..) => panic!(format!("Failed to write to file completions file")), } @@ -18,33 +15,23 @@ macro_rules! w { 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 { + pub fn new(p: &'b Parser<'a, 'b>) -> Self { ComplGen { p: p, - out_dir: od, } } - pub fn generate(&self, for_shell: Shell) { + pub fn generate(&self, for_shell: Shell, buf: &mut W) { match for_shell { - Shell::Bash => self.gen_bash(), + Shell::Bash => self.gen_bash(buf), } } - 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(format!("{}_bash.sh", &*self.p.meta.bin_name.as_ref().unwrap()))) { - Err(why) => panic!("couldn't create bash completion file: {}", - why.description()), - Ok(file) => file, - }; - w!(self, file, format!( + fn gen_bash(&self, buf: &mut W) { + w!(buf, format!( "_{name}() {{ local i cur prev opts cmds COMPREPLY=() diff --git a/tests/completions.rs b/tests/completions.rs new file mode 100644 index 00000000000..c024232268d --- /dev/null +++ b/tests/completions.rs @@ -0,0 +1,25 @@ +extern crate clap; + +use clap::{App, Arg, SubCommand, Shell}; + +#[test] +fn test_generation() { + let mut app = App::new("myapp") + .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"))); + let mut buf = vec![]; + app.gen_completions_to("myapp", Shell::Bash, &mut buf); + let string = String::from_utf8(buf).unwrap(); + let first_line = string.lines().nth(0).unwrap(); + let last_line = string.lines().rev().nth(0).unwrap(); + + assert_eq!(first_line, "_myapp() {"); + assert_eq!(last_line, "complete -F _myapp myapp"); +}