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