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