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
This commit is contained in:
hk 2020-10-30 21:04:17 +01:00
parent 97b4fb639f
commit 1b451f62ad
2 changed files with 98 additions and 20 deletions

View file

@ -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<String>) {
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) {
"*"

View file

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