From 129959fb27d341c06cd8e4a0141adee40a995cfb Mon Sep 17 00:00:00 2001 From: Lzu Tao Date: Sat, 6 Jul 2024 02:57:50 +0700 Subject: [PATCH] feat(fish): Support nested subcommand completion --- clap_complete/src/shells/fish.rs | 149 ++++++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 20 deletions(-) diff --git a/clap_complete/src/shells/fish.rs b/clap_complete/src/shells/fish.rs index a6177ae7df9..88ef9bb3ed6 100644 --- a/clap_complete/src/shells/fish.rs +++ b/clap_complete/src/shells/fish.rs @@ -20,8 +20,32 @@ impl Generator for Fish { .get_bin_name() .expect("crate::generate should have set the bin_name"); + let has_global_flags = cmd.get_arguments().any(|a| { + !a.is_positional() + && a.get_long() + .map(|name| !["help", "version"].contains(&name)) + .unwrap_or(true) + }); + + let name = escape_name(bin_name); + let mut needs_fn_name = &format!("__fish_{name}_needs_command")[..]; + let mut using_fn_name = &format!("__fish_{name}_using_subcommand")[..]; + if has_global_flags { + gen_subcommand_helpers(&name, cmd, buf, needs_fn_name, using_fn_name); + } else { + needs_fn_name = "__fish_use_subcommand"; + using_fn_name = "__fish_seen_subcommand_from"; + } + let mut buffer = String::new(); - gen_fish_inner(bin_name, &[], cmd, &mut buffer); + gen_fish_inner( + bin_name, + &[], + cmd, + &mut buffer, + needs_fn_name, + using_fn_name, + ); w!(buf, buffer.as_bytes()); } } @@ -40,11 +64,17 @@ fn escape_help(help: &builder::StyledStr) -> String { escape_string(&help.to_string().replace('\n', " "), false) } +fn escape_name(name: &str) -> String { + name.replace('-', "_") +} + fn gen_fish_inner( root_command: &str, parent_commands: &[&str], cmd: &Command, buffer: &mut String, + needs_fn_name: &str, + using_fn_name: &str, ) { debug!("gen_fish_inner"); // example : @@ -57,31 +87,43 @@ fn gen_fish_inner( // -a "{possible_arguments}" // -r # if require parameter // -f # don't use file completion - // -n "__fish_use_subcommand" # complete for command "myprog" - // -n "__fish_seen_subcommand_from subcmd1" # complete for command "myprog subcmd1" + // -n "{needs_fn_name}" # complete for command "myprog" + // -n "{using_fn_name} subcmd1" # complete for command "myprog subcmd1" let mut basic_template = format!("complete -c {root_command}"); if parent_commands.is_empty() { if cmd.has_subcommands() { - basic_template.push_str(" -n \"__fish_use_subcommand\""); + basic_template.push_str(&format!(" -n \"{needs_fn_name}\"")); } } else { - let mut out = String::from("__fish_seen_subcommand_from"); - for &command in parent_commands { - out.push(' '); - out.push_str(command); - } - let subcommands: Vec<&str> = cmd - .get_subcommands() - .flat_map(Command::get_name_and_visible_aliases) - .collect(); - if !subcommands.is_empty() { - out.push_str("; and not __fish_seen_subcommand_from"); - } - for name in subcommands { - out.push(' '); - out.push_str(name); + let mut out = String::from(using_fn_name); + match parent_commands { + [] => unreachable!(), + [command] => { + out.push_str(&format!(" {command}")); + if cmd.has_subcommands() { + out.push_str("; and not __fish_seen_subcommand_from"); + } + let subcommands = cmd + .get_subcommands() + .flat_map(Command::get_name_and_visible_aliases); + for name in subcommands { + out.push_str(&format!(" {name}")); + } + } + [command, subcommand] => out.push_str(&format!( + " {command}; and __fish_seen_subcommand_from {subcommand}" + )), + [command, "help", _subcommand] => { + out.push_str(&format!(" {command}; and __fish_seen_subcommand_from help")); + } + ["help", command, _subcommand] => { + out.push_str(&format!(" help; and __fish_seen_subcommand_from {command}")); + } + _ => unimplemented!( + "subcommand should be nested less than 3 levels: {parent_commands:?}" + ), } basic_template.push_str(format!(" -n \"{out}\"").as_str()); } @@ -160,9 +202,76 @@ fn gen_fish_inner( for subcommand_name in subcommand.get_name_and_visible_aliases() { let mut parent_commands: Vec<_> = parent_commands.into(); parent_commands.push(subcommand_name); - gen_fish_inner(root_command, &parent_commands, subcommand, buffer); + gen_fish_inner( + root_command, + &parent_commands, + subcommand, + buffer, + needs_fn_name, + using_fn_name, + ); + } + } +} + +/// Print fish's helpers for easy handling subcommands. +fn gen_subcommand_helpers( + bin_name: &str, + cmd: &Command, + buf: &mut dyn Write, + needs_fn_name: &str, + using_fn_name: &str, +) { + if !cmd.has_subcommands() { + return; + } + let mut opts = String::new(); + let cmd_opts = cmd.get_arguments().filter(|a| !a.is_positional()); + for option in cmd_opts { + opts.push(' '); + let mut has_short = false; + if let Some(short) = option.get_short() { + has_short = true; + opts.push(short); + } + + if let Some(long) = option.get_long() { + if has_short { + opts.push('/'); + } + opts.push_str(&escape_string(long, false)); + } + if option.is_required_set() { + opts.push('='); } } + let optspecs_fn_name = format!("__fish_{bin_name}_global_optspecs"); + let template = format!("\ + # Print an optspec for argparse to handle cmd's options that are independent of any subcommand.\n\ + function {optspecs_fn_name}\n\ + \tstring join \\n{opts}\n\ + end\n\n\ + function {needs_fn_name}\n\ + \t# Figure out if the current invocation already has a command.\n\ + \tset -l cmd (commandline -opc)\n\ + \tset -e cmd[1]\n\ + \targparse -s ({optspecs_fn_name}) -- $cmd 2>/dev/null\n\ + \tor return\n\ + \tif set -q argv[1]\n\ + \t\t# Also print the command, so this can be used to figure out what it is.\n\ + \t\techo $argv[1]\n\ + \t\treturn 1\n\ + \tend\n\ + \treturn 0\n\ + end\n\n\ + function {using_fn_name}\n\ + \tset -l cmd ({needs_fn_name})\n\ + \ttest -z \"$cmd\"\n\ + \tand return 1\n\ + \tcontains -- $cmd[1] $argv\n\ + end\n\n\ + "); + w!(buf, template.as_bytes()); } fn value_completion(option: &Arg) -> String {