diff --git a/src/app/mod.rs b/src/app/mod.rs index 93b3b81aed6..02ef36fef29 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 d51017fc040..29cdfbcfd65 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 00000000000..811fcea7f83 --- /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 22074ad9440..e975c12dc3d 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 + } +}