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> {