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