clap_complete/aot/shells/
fish.rs
1use std::io::{Error, Write};
2
3use clap::{builder, Arg, Command, ValueHint};
4
5use crate::generator::{utils, Generator};
6
7#[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 self.try_generate(cmd, buf)
20 .expect("failed to write completion file");
21 }
22
23 fn try_generate(&self, cmd: &Command, buf: &mut dyn Write) -> Result<(), Error> {
24 let bin_name = cmd
25 .get_bin_name()
26 .expect("crate::generate should have set the bin_name");
27
28 let name = escape_name(bin_name);
29 let mut needs_fn_name = &format!("__fish_{name}_needs_command")[..];
30 let mut using_fn_name = &format!("__fish_{name}_using_subcommand")[..];
31 if cmd.has_subcommands() {
35 gen_subcommand_helpers(&name, cmd, buf, needs_fn_name, using_fn_name);
36 } else {
37 needs_fn_name = "__fish_use_subcommand";
38 using_fn_name = "__fish_seen_subcommand_from";
39 }
40
41 let mut buffer = String::new();
42 gen_fish_inner(
43 bin_name,
44 &[],
45 cmd,
46 &mut buffer,
47 needs_fn_name,
48 using_fn_name,
49 );
50 write!(buf, "{buffer}")
51 }
52}
53
54fn escape_string(string: &str, escape_comma: bool) -> String {
56 let string = string.replace('\\', "\\\\").replace('\'', "\\'");
57 if escape_comma {
58 string.replace(',', "\\,")
59 } else {
60 string
61 }
62}
63
64fn escape_help(help: &builder::StyledStr) -> String {
65 escape_string(&help.to_string().replace('\n', " "), false)
66}
67
68fn escape_name(name: &str) -> String {
69 name.replace('-', "_")
70}
71
72fn gen_fish_inner(
73 root_command: &str,
74 parent_commands: &[&str],
75 cmd: &Command,
76 buffer: &mut String,
77 needs_fn_name: &str,
78 using_fn_name: &str,
79) {
80 debug!("gen_fish_inner");
81 let mut basic_template = format!("complete -c {root_command}");
95
96 if parent_commands.is_empty() {
97 if cmd.has_subcommands() {
98 basic_template.push_str(&format!(" -n \"{needs_fn_name}\""));
99 }
100 } else {
101 let mut out = String::from(using_fn_name);
102 match parent_commands {
103 [] => unreachable!(),
104 [command] => {
105 out.push_str(&format!(" {command}"));
106 if cmd.has_subcommands() {
107 out.push_str("; and not __fish_seen_subcommand_from");
108 }
109 let subcommands = cmd
110 .get_subcommands()
111 .flat_map(Command::get_name_and_visible_aliases);
112 for name in subcommands {
113 out.push_str(&format!(" {name}"));
114 }
115 }
116 [command, subcommand] => out.push_str(&format!(
117 " {command}; and __fish_seen_subcommand_from {subcommand}"
118 )),
119 _ => return,
124 }
125 basic_template.push_str(format!(" -n \"{out}\"").as_str());
126 }
127
128 debug!("gen_fish_inner: parent_commands={parent_commands:?}");
129
130 for option in cmd.get_opts() {
131 let mut template = basic_template.clone();
132
133 if let Some(shorts) = option.get_short_and_visible_aliases() {
134 for short in shorts {
135 template.push_str(format!(" -s {short}").as_str());
136 }
137 }
138
139 if let Some(longs) = option.get_long_and_visible_aliases() {
140 for long in longs {
141 template.push_str(format!(" -l {}", escape_string(long, false)).as_str());
142 }
143 }
144
145 if let Some(data) = option.get_help() {
146 template.push_str(&format!(" -d '{}'", escape_help(data)));
147 }
148
149 template.push_str(value_completion(option).as_str());
150
151 buffer.push_str(template.as_str());
152 buffer.push('\n');
153 }
154
155 for flag in utils::flags(cmd) {
156 let mut template = basic_template.clone();
157
158 if let Some(shorts) = flag.get_short_and_visible_aliases() {
159 for short in shorts {
160 template.push_str(format!(" -s {short}").as_str());
161 }
162 }
163
164 if let Some(longs) = flag.get_long_and_visible_aliases() {
165 for long in longs {
166 template.push_str(format!(" -l {}", escape_string(long, false)).as_str());
167 }
168 }
169
170 if let Some(data) = flag.get_help() {
171 template.push_str(&format!(" -d '{}'", escape_help(data)));
172 }
173
174 buffer.push_str(template.as_str());
175 buffer.push('\n');
176 }
177
178 let has_positionals = cmd.get_positionals().next().is_some();
179 if !has_positionals {
180 basic_template.push_str(" -f");
181 }
182 for subcommand in cmd.get_subcommands() {
183 for subcommand_name in subcommand.get_name_and_visible_aliases() {
184 let mut template = basic_template.clone();
185
186 template.push_str(format!(" -a \"{subcommand_name}\"").as_str());
187
188 if let Some(data) = subcommand.get_about() {
189 template.push_str(format!(" -d '{}'", escape_help(data)).as_str());
190 }
191
192 buffer.push_str(template.as_str());
193 buffer.push('\n');
194 }
195 }
196
197 for subcommand in cmd.get_subcommands() {
199 for subcommand_name in subcommand.get_name_and_visible_aliases() {
200 let mut parent_commands: Vec<_> = parent_commands.into();
201 parent_commands.push(subcommand_name);
202 gen_fish_inner(
203 root_command,
204 &parent_commands,
205 subcommand,
206 buffer,
207 needs_fn_name,
208 using_fn_name,
209 );
210 }
211 }
212}
213
214fn gen_subcommand_helpers(
216 bin_name: &str,
217 cmd: &Command,
218 buf: &mut dyn Write,
219 needs_fn_name: &str,
220 using_fn_name: &str,
221) {
222 let mut optspecs = String::new();
223 let cmd_opts = cmd.get_arguments().filter(|a| !a.is_positional());
224 for option in cmd_opts {
225 optspecs.push(' ');
226 let mut has_short = false;
227 if let Some(short) = option.get_short() {
228 has_short = true;
229 optspecs.push(short);
230 }
231
232 if let Some(long) = option.get_long() {
233 if has_short {
234 optspecs.push('/');
235 }
236 optspecs.push_str(&escape_string(long, false));
237 }
238
239 let is_an_option = option
240 .get_num_args()
241 .map(|r| r.takes_values())
242 .unwrap_or(true);
243 if is_an_option {
244 optspecs.push('=');
245 }
246 }
247 let optspecs_fn_name = format!("__fish_{bin_name}_global_optspecs");
248 write!(
249 buf,
250 "\
251 # Print an optspec for argparse to handle cmd's options that are independent of any subcommand.\n\
252 function {optspecs_fn_name}\n\
253 \tstring join \\n{optspecs}\n\
254 end\n\n\
255 function {needs_fn_name}\n\
256 \t# Figure out if the current invocation already has a command.\n\
257 \tset -l cmd (commandline -opc)\n\
258 \tset -e cmd[1]\n\
259 \targparse -s ({optspecs_fn_name}) -- $cmd 2>/dev/null\n\
260 \tor return\n\
261 \tif set -q argv[1]\n\
262 \t\t# Also print the command, so this can be used to figure out what it is.\n\
263 \t\techo $argv[1]\n\
264 \t\treturn 1\n\
265 \tend\n\
266 \treturn 0\n\
267 end\n\n\
268 function {using_fn_name}\n\
269 \tset -l cmd ({needs_fn_name})\n\
270 \ttest -z \"$cmd\"\n\
271 \tand return 1\n\
272 \tcontains -- $cmd[1] $argv\n\
273 end\n\n\
274 ").expect("failed to write completion file");
275}
276
277fn value_completion(option: &Arg) -> String {
278 if !option.get_num_args().expect("built").takes_values() {
279 return "".to_string();
280 }
281
282 if let Some(data) = utils::possible_values(option) {
283 format!(
286 " -r -f -a \"{}\"",
287 data.iter()
288 .filter_map(|value| if value.is_hide_set() {
289 None
290 } else {
291 Some(format!(
294 "{}\\t'{}'",
295 escape_string(value.get_name(), true).as_str(),
296 escape_help(value.get_help().unwrap_or_default())
297 ))
298 })
299 .collect::<Vec<_>>()
300 .join("\n")
301 )
302 } else {
303 match option.get_value_hint() {
305 ValueHint::Unknown => " -r",
306 ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
308 ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
309 ValueHint::CommandString | ValueHint::CommandName => {
312 " -r -f -a \"(__fish_complete_command)\""
313 }
314 ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
315 ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
316 _ => " -r -f",
318 }
319 .to_string()
320 }
321}