Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fish/complete: Generate helper functions to check for subcommands #5568

Merged
merged 9 commits into from
Jul 11, 2024
143 changes: 123 additions & 20 deletions clap_complete/src/shells/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,28 @@ impl Generator for Fish {
.get_bin_name()
.expect("crate::generate should have set the bin_name");

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")[..];
// Given `git --git-dir somedir status`, using `__fish_seen_subcommand_from` won't help us
// find out `status` is the real subcommand, and not `somedir`. However, when there are no subcommands,
// there is no need to use our custom stubs.
if cmd.has_subcommands() {
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());
}
}
Expand All @@ -40,11 +60,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 :
Expand All @@ -57,31 +83,39 @@ 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}"
)),
// HACK: Assuming subcommands are only nested less than 3 levels as more than that is
// unwieldy and takes more effort to support.
// For example, `rustup toolchain help install` is the longest valid command line of `rustup`
// that uses nested subcommands, and it cannot receive any flags to it.
_ => return,
}
basic_template.push_str(format!(" -n \"{out}\"").as_str());
}
Expand Down Expand Up @@ -160,9 +194,78 @@ 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,
) {
let mut optspecs = String::new();
let cmd_opts = cmd.get_arguments().filter(|a| !a.is_positional());
for option in cmd_opts {
optspecs.push(' ');
let mut has_short = false;
if let Some(short) = option.get_short() {
has_short = true;
optspecs.push(short);
}

if let Some(long) = option.get_long() {
if has_short {
optspecs.push('/');
}
optspecs.push_str(&escape_string(long, false));
}

let is_an_option = option
.get_num_args()
.map(|r| r.takes_values())
.unwrap_or(true);
if is_an_option {
optspecs.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{optspecs}\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 {
Expand Down
46 changes: 36 additions & 10 deletions clap_complete/tests/snapshots/basic.fish
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
complete -c my-app -n "__fish_use_subcommand" -s c
complete -c my-app -n "__fish_use_subcommand" -s v
complete -c my-app -n "__fish_use_subcommand" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_use_subcommand" -f -a "test" -d 'Subcommand with a second line'
complete -c my-app -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c my-app -n "__fish_seen_subcommand_from test" -s d
complete -c my-app -n "__fish_seen_subcommand_from test" -s c
complete -c my-app -n "__fish_seen_subcommand_from test" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from test help" -f -a "test" -d 'Subcommand with a second line'
complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from test help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_my_app_global_optspecs
tesuji marked this conversation as resolved.
Show resolved Hide resolved
string join \n c v h/help
end

function __fish_my_app_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_my_app_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end

function __fish_my_app_using_subcommand
set -l cmd (__fish_my_app_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end

complete -c my-app -n "__fish_my_app_needs_command" -s c
complete -c my-app -n "__fish_my_app_needs_command" -s v
complete -c my-app -n "__fish_my_app_needs_command" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_my_app_needs_command" -f -a "test" -d 'Subcommand with a second line'
complete -c my-app -n "__fish_my_app_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c my-app -n "__fish_my_app_using_subcommand test" -s d
complete -c my-app -n "__fish_my_app_using_subcommand test" -s c
complete -c my-app -n "__fish_my_app_using_subcommand test" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_my_app_using_subcommand help; and not __fish_seen_subcommand_from test help" -f -a "test" -d 'Subcommand with a second line'
complete -c my-app -n "__fish_my_app_using_subcommand help; and not __fish_seen_subcommand_from test help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
46 changes: 36 additions & 10 deletions clap_complete/tests/snapshots/custom_bin_name.fish
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
complete -c bin-name -n "__fish_use_subcommand" -s c
complete -c bin-name -n "__fish_use_subcommand" -s v
complete -c bin-name -n "__fish_use_subcommand" -s h -l help -d 'Print help'
complete -c bin-name -n "__fish_use_subcommand" -f -a "test" -d 'Subcommand with a second line'
complete -c bin-name -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c bin-name -n "__fish_seen_subcommand_from test" -s d
complete -c bin-name -n "__fish_seen_subcommand_from test" -s c
complete -c bin-name -n "__fish_seen_subcommand_from test" -s h -l help -d 'Print help'
complete -c bin-name -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from test help" -f -a "test" -d 'Subcommand with a second line'
complete -c bin-name -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from test help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_bin_name_global_optspecs
string join \n c v h/help
end

function __fish_bin_name_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_bin_name_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end

function __fish_bin_name_using_subcommand
set -l cmd (__fish_bin_name_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end

complete -c bin-name -n "__fish_bin_name_needs_command" -s c
complete -c bin-name -n "__fish_bin_name_needs_command" -s v
complete -c bin-name -n "__fish_bin_name_needs_command" -s h -l help -d 'Print help'
complete -c bin-name -n "__fish_bin_name_needs_command" -f -a "test" -d 'Subcommand with a second line'
complete -c bin-name -n "__fish_bin_name_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c bin-name -n "__fish_bin_name_using_subcommand test" -s d
complete -c bin-name -n "__fish_bin_name_using_subcommand test" -s c
complete -c bin-name -n "__fish_bin_name_using_subcommand test" -s h -l help -d 'Print help'
complete -c bin-name -n "__fish_bin_name_using_subcommand help; and not __fish_seen_subcommand_from test help" -f -a "test" -d 'Subcommand with a second line'
complete -c bin-name -n "__fish_bin_name_using_subcommand help; and not __fish_seen_subcommand_from test help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
46 changes: 36 additions & 10 deletions clap_complete/tests/snapshots/feature_sample.fish
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
complete -c my-app -n "__fish_use_subcommand" -s c -s C -l config -l conf -d 'some config file'
complete -c my-app -n "__fish_use_subcommand" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_use_subcommand" -s V -l version -d 'Print version'
complete -c my-app -n "__fish_use_subcommand" -a "test" -d 'tests things'
complete -c my-app -n "__fish_use_subcommand" -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c my-app -n "__fish_seen_subcommand_from test" -l case -d 'the case to test' -r
complete -c my-app -n "__fish_seen_subcommand_from test" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_seen_subcommand_from test" -s V -l version -d 'Print version'
complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from test help" -f -a "test" -d 'tests things'
complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from test help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_my_app_global_optspecs
string join \n c/config h/help V/version
end

function __fish_my_app_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_my_app_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end

function __fish_my_app_using_subcommand
set -l cmd (__fish_my_app_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end

complete -c my-app -n "__fish_my_app_needs_command" -s c -s C -l config -l conf -d 'some config file'
complete -c my-app -n "__fish_my_app_needs_command" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_my_app_needs_command" -s V -l version -d 'Print version'
complete -c my-app -n "__fish_my_app_needs_command" -a "test" -d 'tests things'
complete -c my-app -n "__fish_my_app_needs_command" -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c my-app -n "__fish_my_app_using_subcommand test" -l case -d 'the case to test' -r
complete -c my-app -n "__fish_my_app_using_subcommand test" -s h -l help -d 'Print help'
complete -c my-app -n "__fish_my_app_using_subcommand test" -s V -l version -d 'Print version'
complete -c my-app -n "__fish_my_app_using_subcommand help; and not __fish_seen_subcommand_from test help" -f -a "test" -d 'tests things'
complete -c my-app -n "__fish_my_app_using_subcommand help; and not __fish_seen_subcommand_from test help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
Loading
Loading