From 1b451f62ada20539aa973f7a2d38c7cd4d3f100f Mon Sep 17 00:00:00 2001 From: hk Date: Fri, 30 Oct 2020 21:04:17 +0100 Subject: [PATCH] fix(clap_generate): zsh completion generation panic zsh completion generation would panic if a global argument had conflicts with another argument which was present in its own command but not in its subcommands --- clap_generate/src/generators/shells/zsh.rs | 59 ++++++++++++++-------- src/build/app/mod.rs | 59 ++++++++++++++++++++++ 2 files changed, 98 insertions(+), 20 deletions(-) diff --git a/clap_generate/src/generators/shells/zsh.rs b/clap_generate/src/generators/shells/zsh.rs index 331c4497..0ce438d0 100644 --- a/clap_generate/src/generators/shells/zsh.rs +++ b/clap_generate/src/generators/shells/zsh.rs @@ -43,7 +43,7 @@ _{name}() {{ _{name} \"$@\"", name = app.get_bin_name().unwrap(), - initial_args = get_args_of(app), + initial_args = get_args_of(app, None), subcommands = get_subcommands_of(app), subcommand_details = subcommand_details(app) ) @@ -225,7 +225,8 @@ fn get_subcommands_of(p: &App) -> String { bin_name, ); let mut v = vec![format!("({})", name)]; - let subcommand_args = get_args_of(parser_of(p, &*bin_name).expect(INTERNAL_ERROR_MSG)); + let subcommand_args = + get_args_of(parser_of(p, &*bin_name).expect(INTERNAL_ERROR_MSG), Some(p)); if !subcommand_args.is_empty() { v.push(subcommand_args); @@ -299,12 +300,12 @@ fn parser_of<'help, 'app>(p: &'app App<'help>, bin_name: &str) -> Option<&'app A // -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 { +fn get_args_of(p: &App, p_global: Option<&App>) -> String { debug!("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 opts = write_opts_of(p, p_global); + let flags = write_flags_of(p, p_global); let positionals = write_positionals_of(p); let sc_or_a = if p.has_subcommands() { @@ -401,7 +402,7 @@ fn escape_value(string: &str) -> String { .replace(" ", "\\ ") } -fn write_opts_of(p: &App) -> String { +fn write_opts_of(p: &App, p_global: Option<&App>) -> String { debug!("write_opts_of"); let mut ret = vec![]; @@ -410,7 +411,7 @@ fn write_opts_of(p: &App) -> String { debug!("write_opts_of:iter: o={}", o.get_name()); let help = o.get_about().map_or(String::new(), escape_help); - let conflicts = arg_conflicts(p, o); + let conflicts = arg_conflicts(p, o, p_global); // @TODO @soundness should probably be either multiple occurrences or multiple values and // not both @@ -475,28 +476,46 @@ fn write_opts_of(p: &App) -> String { ret.join("\n") } -fn arg_conflicts(app: &App, arg: &Arg) -> String { - let conflicts = app.get_arg_conflicts_with(arg); +fn arg_conflicts(app: &App, arg: &Arg, app_global: Option<&App>) -> String { + fn push_conflicts(conflicts: &[&Arg], res: &mut Vec) { + for conflict in conflicts { + if let Some(s) = conflict.get_short() { + res.push(format!("-{}", s)); + } - if conflicts.is_empty() { - return String::new(); + if let Some(l) = conflict.get_long() { + res.push(format!("--{}", l)); + } + } } let mut res = vec![]; - for conflict in conflicts { - if let Some(s) = conflict.get_short() { - res.push(format!("-{}", s)); - } + match (app_global, App::test_global(arg)) { + (Some(x), true) => { + let conflicts = x.get_global_arg_conflicts_with(arg); + // let conflicts = app.get_arg_conflicts_with(arg); - if let Some(l) = conflict.get_long() { - res.push(format!("--{}", l)); + if conflicts.is_empty() { + return String::new(); + } + + push_conflicts(&conflicts, &mut res); } - } + (_, _) => { + let conflicts = app.get_arg_conflicts_with(arg); + + if conflicts.is_empty() { + return String::new(); + } + + push_conflicts(&conflicts, &mut res); + } + }; format!("({})", res.join(" ")) } -fn write_flags_of(p: &App) -> String { +fn write_flags_of(p: &App, p_global: Option<&App>) -> String { debug!("write_flags_of;"); let mut ret = vec![]; @@ -505,7 +524,7 @@ fn write_flags_of(p: &App) -> String { debug!("write_flags_of:iter: f={}", f.get_name()); let help = f.get_about().map_or(String::new(), escape_help); - let conflicts = arg_conflicts(p, &f); + let conflicts = arg_conflicts(p, &f, p_global); let multiple = if f.is_set(ArgSettings::MultipleOccurrences) { "*" diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index d92795cc..2b615280 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -236,6 +236,65 @@ impl<'help> App<'help> { self.get_opts().filter(|a| a.get_help_heading().is_none()) } + /// Test if the provided Argument is a global Argument + pub fn test_global(arg: &Arg) -> bool { + arg.global + } + + /// Get a list of subcommands which contain the provided Argument + /// + /// This command will only include subcommands in its list for which the subcommands + /// parent also contains the Argument. + /// + /// **NOTE:** In this case only Sucommand_1 will be included + /// Subcommand_1 (contains Arg) + /// Subcommand_1.1 (doesn't contain Arg) + /// Subcommand_1.1.1 (contains Arg) + /// + pub fn get_subcommands_containing(&self, arg: &Arg) -> Vec<&App<'help>> { + let mut vec = std::vec::Vec::new(); + for idx in 0..self.subcommands.len() { + if self.subcommands[idx] + .args + .args + .iter() + .any(|ar| ar.id == arg.id) + { + vec.push(&self.subcommands[idx]); + vec.append(&mut self.subcommands[idx].get_subcommands_containing(arg)); + } + } + vec + } + + /// Get a unique list of all arguments of all commands and continuous subcommands the given argument conflicts with. + /// + /// ### Panics + /// + /// If the given arg contains a conflict with an argument that is unknown to + /// this `App`. + pub fn get_global_arg_conflicts_with(&self, arg: &Arg) -> Vec<&Arg<'help>> // FIXME: This could probably have been an iterator + { + arg.blacklist + .iter() + .map(|id| { + self.args + .args + .iter() + .chain( + self.get_subcommands_containing(arg) + .iter() + .flat_map(|x| x.args.args.iter()), + ) + .find(|arg| arg.id == *id) + .expect( + "App::get_arg_conflicts_with: \ + The passed arg conflicts with an arg unknown to the app", + ) + }) + .collect() + } + /// Get a list of all arguments the given argument conflicts with. /// /// ### Panics