use std::io::Write;
use clap::{builder, Arg, Command, ValueHint};
use crate::generator::{utils, Generator};
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Fish;
impl Generator for Fish {
fn file_name(&self, name: &str) -> String {
format!("{name}.fish")
}
fn generate(&self, cmd: &Command, buf: &mut dyn Write) {
let bin_name = cmd
.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")[..];
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,
needs_fn_name,
using_fn_name,
);
write!(buf, "{buffer}").expect("failed to write completion file");
}
}
fn escape_string(string: &str, escape_comma: bool) -> String {
let string = string.replace('\\', "\\\\").replace('\'', "\\'");
if escape_comma {
string.replace(',', "\\,")
} else {
string
}
}
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");
let mut basic_template = format!("complete -c {root_command}");
if parent_commands.is_empty() {
if cmd.has_subcommands() {
basic_template.push_str(&format!(" -n \"{needs_fn_name}\""));
}
} else {
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}"
)),
_ => return,
}
basic_template.push_str(format!(" -n \"{out}\"").as_str());
}
debug!("gen_fish_inner: parent_commands={parent_commands:?}");
for option in cmd.get_opts() {
let mut template = basic_template.clone();
if let Some(shorts) = option.get_short_and_visible_aliases() {
for short in shorts {
template.push_str(format!(" -s {short}").as_str());
}
}
if let Some(longs) = option.get_long_and_visible_aliases() {
for long in longs {
template.push_str(format!(" -l {}", escape_string(long, false)).as_str());
}
}
if let Some(data) = option.get_help() {
template.push_str(&format!(" -d '{}'", escape_help(data)));
}
template.push_str(value_completion(option).as_str());
buffer.push_str(template.as_str());
buffer.push('\n');
}
for flag in utils::flags(cmd) {
let mut template = basic_template.clone();
if let Some(shorts) = flag.get_short_and_visible_aliases() {
for short in shorts {
template.push_str(format!(" -s {short}").as_str());
}
}
if let Some(longs) = flag.get_long_and_visible_aliases() {
for long in longs {
template.push_str(format!(" -l {}", escape_string(long, false)).as_str());
}
}
if let Some(data) = flag.get_help() {
template.push_str(&format!(" -d '{}'", escape_help(data)));
}
buffer.push_str(template.as_str());
buffer.push('\n');
}
let has_positionals = cmd.get_positionals().next().is_some();
if !has_positionals {
basic_template.push_str(" -f");
}
for subcommand in cmd.get_subcommands() {
for subcommand_name in subcommand.get_name_and_visible_aliases() {
let mut template = basic_template.clone();
template.push_str(format!(" -a \"{subcommand_name}\"").as_str());
if let Some(data) = subcommand.get_about() {
template.push_str(format!(" -d '{}'", escape_help(data)).as_str());
}
buffer.push_str(template.as_str());
buffer.push('\n');
}
}
for subcommand in cmd.get_subcommands() {
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,
needs_fn_name,
using_fn_name,
);
}
}
}
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");
write!(
buf,
"\
# 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\
").expect("failed to write completion file");
}
fn value_completion(option: &Arg) -> String {
if !option.get_num_args().expect("built").takes_values() {
return "".to_string();
}
if let Some(data) = utils::possible_values(option) {
format!(
" -r -f -a \"{{{}}}\"",
data.iter()
.filter_map(|value| if value.is_hide_set() {
None
} else {
Some(format!(
"{}\\t'{}'",
escape_string(value.get_name(), true).as_str(),
escape_help(value.get_help().unwrap_or_default())
))
})
.collect::<Vec<_>>()
.join(",")
)
} else {
match option.get_value_hint() {
ValueHint::Unknown => " -r",
ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
ValueHint::CommandString | ValueHint::CommandName => {
" -r -f -a \"(__fish_complete_command)\""
}
ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
_ => " -r -f",
}
.to_string()
}
}