clap_complete/aot/shells/
fish.rs1use std::io::{Error, Write};
2
3use clap::{Arg, Command, ValueHint, builder};
4
5use crate::generator::{Generator, utils};
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 "# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
251function {optspecs_fn_name}
252 string join \\n{optspecs}
253end
254
255function {needs_fn_name}
256 # Figure out if the current invocation already has a command.
257 set -l cmd (commandline -opc)
258 set -e cmd[1]
259 argparse -s ({optspecs_fn_name}) -- $cmd 2>/dev/null
260 or return
261 if set -q argv[1]
262 # Also print the command, so this can be used to figure out what it is.
263 echo $argv[1]
264 return 1
265 end
266 return 0
267end
268
269function {using_fn_name}
270 set -l cmd ({needs_fn_name})
271 test -z \"$cmd\"
272 and return 1
273 contains -- $cmd[1] $argv
274end
275
276"
277 ).expect("failed to write completion file");
278}
279
280fn value_completion(option: &Arg) -> String {
281 if !option.get_num_args().expect("built").takes_values() {
282 return "".to_owned();
283 }
284
285 if let Some(data) = utils::possible_values(option) {
286 format!(
289 " -r -f -a \"{}\"",
290 data.iter()
291 .filter_map(|value| if value.is_hide_set() {
292 None
293 } else {
294 Some(format!(
297 "{}\\t'{}'",
298 escape_string(value.get_name(), true).as_str(),
299 escape_help(value.get_help().unwrap_or_default())
300 ))
301 })
302 .collect::<Vec<_>>()
303 .join("\n")
304 )
305 } else {
306 match option.get_value_hint() {
308 ValueHint::Unknown => " -r",
309 ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
311 ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
312 ValueHint::CommandString | ValueHint::CommandName => {
315 " -r -f -a \"(__fish_complete_command)\""
316 }
317 ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
318 ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
319 _ => " -r -f",
321 }
322 .to_owned()
323 }
324}