summaryrefslogtreecommitdiff
path: root/src/shells/fish.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/shells/fish.rs')
-rw-r--r--src/shells/fish.rs201
1 files changed, 201 insertions, 0 deletions
diff --git a/src/shells/fish.rs b/src/shells/fish.rs
new file mode 100644
index 0000000..7dae5b6
--- /dev/null
+++ b/src/shells/fish.rs
@@ -0,0 +1,201 @@
+use std::io::Write;
+
+use clap::*;
+
+use crate::generator::{utils, Generator};
+
+/// Generate fish completion file
+///
+/// Note: The fish generator currently only supports named options (-o/--option), not positional arguments.
+#[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 mut buffer = String::new();
+ gen_fish_inner(bin_name, &[], cmd, &mut buffer);
+ w!(buf, buffer.as_bytes());
+ }
+}
+
+// Escape string inside single quotes
+fn escape_string(string: &str, escape_comma: bool) -> String {
+ let string = string.replace('\\', "\\\\").replace('\'', "\\'");
+ if escape_comma {
+ string.replace(',', "\\,")
+ } else {
+ string
+ }
+}
+
+fn gen_fish_inner(
+ root_command: &str,
+ parent_commands: &[&str],
+ cmd: &Command,
+ buffer: &mut String,
+) {
+ debug!("gen_fish_inner");
+ // example :
+ //
+ // complete
+ // -c {command}
+ // -d "{description}"
+ // -s {short}
+ // -l {long}
+ // -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"
+
+ 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\"");
+ }
+ } else {
+ basic_template.push_str(
+ format!(
+ " -n \"{}\"",
+ parent_commands
+ .iter()
+ .map(|command| format!("__fish_seen_subcommand_from {command}"))
+ .chain(
+ cmd.get_subcommands()
+ .map(|command| format!("not __fish_seen_subcommand_from {command}"))
+ )
+ .collect::<Vec<_>>()
+ .join("; and ")
+ )
+ .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_string(&data.to_string(), false)).as_str());
+ }
+
+ 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_string(&data.to_string(), false)).as_str());
+ }
+
+ buffer.push_str(template.as_str());
+ buffer.push('\n');
+ }
+
+ for subcommand in cmd.get_subcommands() {
+ let mut template = basic_template.clone();
+
+ template.push_str(" -f");
+ template.push_str(format!(" -a \"{}\"", &subcommand.get_name()).as_str());
+
+ if let Some(data) = subcommand.get_about() {
+ template.push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str())
+ }
+
+ buffer.push_str(template.as_str());
+ buffer.push('\n');
+ }
+
+ // generate options of subcommands
+ for subcommand in cmd.get_subcommands() {
+ let mut parent_commands: Vec<_> = parent_commands.into();
+ parent_commands.push(subcommand.get_name());
+ gen_fish_inner(root_command, &parent_commands, subcommand, buffer);
+ }
+}
+
+fn value_completion(option: &Arg) -> String {
+ if !option.get_num_args().expect("built").takes_values() {
+ return "".to_string();
+ }
+
+ if let Some(data) = crate::generator::utils::possible_values(option) {
+ // We return the possible values with their own empty description e.g. {a\t,b\t}
+ // this makes sure that a and b don't get the description of the option or argument
+ format!(
+ " -r -f -a \"{{{}}}\"",
+ data.iter()
+ .filter_map(|value| if value.is_hide_set() {
+ None
+ } else {
+ // The help text after \t is wrapped in '' to make sure that the it is taken literally
+ // and there is no command substitution or variable expansion resulting in unexpected errors
+ Some(format!(
+ "{}\t'{}'",
+ escape_string(value.get_name(), true).as_str(),
+ escape_string(&value.get_help().unwrap_or_default().to_string(), false)
+ ))
+ })
+ .collect::<Vec<_>>()
+ .join(",")
+ )
+ } else {
+ // NB! If you change this, please also update the table in `ValueHint` documentation.
+ match option.get_value_hint() {
+ ValueHint::Unknown => " -r",
+ // fish has no built-in support to distinguish these
+ ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
+ ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
+ // It seems fish has no built-in support for completing command + arguments as
+ // single string (CommandString). Complete just the command name.
+ 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)\"",
+ // Disable completion for others
+ _ => " -r -f",
+ }
+ .to_string()
+ }
+}