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: &str = cmd
20 .get_bin_name()
21 .expect(msg:"crate::generate should have set the bin_name");
22
23 let mut buffer: String = String::new();
24 gen_fish_inner(root_command: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 = string.replace('\\', "\\\\").replace(from:'\'', to:"\\'");
32 if escape_comma {
33 string.replace(from:',', to:"\\,")
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 // The help text after \t is wrapped in '' to make sure that the it is taken literally
172 // and there is no command substitution or variable expansion resulting in unexpected errors
173 Some(format!(
174 "{}\t'{}'",
175 escape_string(value.get_name(), true).as_str(),
176 escape_string(&value.get_help().unwrap_or_default().to_string(), false)
177 ))
178 })
179 .collect::<Vec<_>>()
180 .join(",")
181 )
182 } else {
183 // NB! If you change this, please also update the table in `ValueHint` documentation.
184 match option.get_value_hint() {
185 ValueHint::Unknown => " -r",
186 // fish has no built-in support to distinguish these
187 ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
188 ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
189 // It seems fish has no built-in support for completing command + arguments as
190 // single string (CommandString). Complete just the command name.
191 ValueHint::CommandString | ValueHint::CommandName => {
192 " -r -f -a \"(__fish_complete_command)\""
193 }
194 ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
195 ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
196 // Disable completion for others
197 _ => " -r -f",
198 }
199 .to_string()
200 }
201}
202