1use std::io::Write; 2 3use clap::*; 4 5use crate::generator::{utils, Generator}; 6 7/// Generate fish completion file 8/// 9/// Note: The fish generator currently only supports named options (-o/--option), not positional arguments. 10#[derive(Copy, Clone, PartialEq, Eq, Debug)] 11pub struct Fish; 12 13impl Generator for Fish { 14 fn file_name(&self, name: &str) -> String { 15 format!("{name}.fish") 16 } 17 18 fn generate(&self, cmd: &Command, buf: &mut dyn Write) { 19 let bin_name = cmd 20 .get_bin_name() 21 .expect("crate::generate should have set the bin_name"); 22 23 let mut buffer = String::new(); 24 gen_fish_inner(bin_name, &[], cmd, &mut buffer); 25 w!(buf, buffer.as_bytes()); 26 } 27} 28 29// Escape string inside single quotes 30fn escape_string(string: &str, escape_comma: bool) -> String { 31 let string = string.replace('\\', "\\\\").replace('\'', "\\'"); 32 if escape_comma { 33 string.replace(',', "\\,") 34 } else { 35 string 36 } 37} 38 39fn gen_fish_inner( 40 root_command: &str, 41 parent_commands: &[&str], 42 cmd: &Command, 43 buffer: &mut String, 44) { 45 debug!("gen_fish_inner"); 46 // example : 47 // 48 // complete 49 // -c {command} 50 // -d "{description}" 51 // -s {short} 52 // -l {long} 53 // -a "{possible_arguments}" 54 // -r # if require parameter 55 // -f # don't use file completion 56 // -n "__fish_use_subcommand" # complete for command "myprog" 57 // -n "__fish_seen_subcommand_from subcmd1" # complete for command "myprog subcmd1" 58 59 let mut basic_template = format!("complete -c {root_command}"); 60 61 if parent_commands.is_empty() { 62 if cmd.has_subcommands() { 63 basic_template.push_str(" -n \"__fish_use_subcommand\""); 64 } 65 } else { 66 basic_template.push_str( 67 format!( 68 " -n \"{}\"", 69 parent_commands 70 .iter() 71 .map(|command| format!("__fish_seen_subcommand_from {command}")) 72 .chain( 73 cmd.get_subcommands() 74 .map(|command| format!("not __fish_seen_subcommand_from {command}")) 75 ) 76 .collect::<Vec<_>>() 77 .join("; and ") 78 ) 79 .as_str(), 80 ); 81 } 82 83 debug!("gen_fish_inner: parent_commands={:?}", parent_commands); 84 85 for option in cmd.get_opts() { 86 let mut template = basic_template.clone(); 87 88 if let Some(shorts) = option.get_short_and_visible_aliases() { 89 for short in shorts { 90 template.push_str(format!(" -s {short}").as_str()); 91 } 92 } 93 94 if let Some(longs) = option.get_long_and_visible_aliases() { 95 for long in longs { 96 template.push_str(format!(" -l {}", escape_string(long, false)).as_str()); 97 } 98 } 99 100 if let Some(data) = option.get_help() { 101 template 102 .push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str()); 103 } 104 105 template.push_str(value_completion(option).as_str()); 106 107 buffer.push_str(template.as_str()); 108 buffer.push('\n'); 109 } 110 111 for flag in utils::flags(cmd) { 112 let mut template = basic_template.clone(); 113 114 if let Some(shorts) = flag.get_short_and_visible_aliases() { 115 for short in shorts { 116 template.push_str(format!(" -s {short}").as_str()); 117 } 118 } 119 120 if let Some(longs) = flag.get_long_and_visible_aliases() { 121 for long in longs { 122 template.push_str(format!(" -l {}", escape_string(long, false)).as_str()); 123 } 124 } 125 126 if let Some(data) = flag.get_help() { 127 template 128 .push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str()); 129 } 130 131 buffer.push_str(template.as_str()); 132 buffer.push('\n'); 133 } 134 135 for subcommand in cmd.get_subcommands() { 136 let mut template = basic_template.clone(); 137 138 template.push_str(" -f"); 139 template.push_str(format!(" -a \"{}\"", &subcommand.get_name()).as_str()); 140 141 if let Some(data) = subcommand.get_about() { 142 template.push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str()) 143 } 144 145 buffer.push_str(template.as_str()); 146 buffer.push('\n'); 147 } 148 149 // generate options of subcommands 150 for subcommand in cmd.get_subcommands() { 151 let mut parent_commands: Vec<_> = parent_commands.into(); 152 parent_commands.push(subcommand.get_name()); 153 gen_fish_inner(root_command, &parent_commands, subcommand, buffer); 154 } 155} 156 157fn value_completion(option: &Arg) -> String { 158 if !option.get_num_args().expect("built").takes_values() { 159 return "".to_string(); 160 } 161 162 if let Some(data) = crate::generator::utils::possible_values(option) { 163 // We return the possible values with their own empty description e.g. {a\t,b\t} 164 // this makes sure that a and b don't get the description of the option or argument 165 format!( 166 " -r -f -a \"{{{}}}\"", 167 data.iter() 168 .filter_map(|value| if value.is_hide_set() { 169 None 170 } else { 171 Some(format!( 172 "{}\t{}", 173 escape_string(value.get_name(), true).as_str(), 174 escape_string(&value.get_help().unwrap_or_default().to_string(), true) 175 )) 176 }) 177 .collect::<Vec<_>>() 178 .join(",") 179 ) 180 } else { 181 // NB! If you change this, please also update the table in `ValueHint` documentation. 182 match option.get_value_hint() { 183 ValueHint::Unknown => " -r", 184 // fish has no built-in support to distinguish these 185 ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F", 186 ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"", 187 // It seems fish has no built-in support for completing command + arguments as 188 // single string (CommandString). Complete just the command name. 189 ValueHint::CommandString | ValueHint::CommandName => { 190 " -r -f -a \"(__fish_complete_command)\"" 191 } 192 ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"", 193 ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"", 194 // Disable completion for others 195 _ => " -r -f", 196 } 197 .to_string() 198 } 199} 200