1 | use std::io::Write; |
2 | |
3 | use clap::*; |
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 { |
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 |
30 | fn 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 | |
39 | fn 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 | |
157 | fn 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 | |