• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
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 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 
value_completion(option: &Arg) -> String157 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                     Some(format!(
172                         "{}\t{}",
173                         escape_string(value.get_name(), true).as_str(),
174                         escape_string(&value.get_help().unwrap_or_default().to_string(), true)
175                     ))
176                 })
177                 .collect::<Vec<_>>()
178                 .join(",")
179         )
180     } else {
181         // NB! If you change this, please also update the table in `ValueHint` documentation.
182         match option.get_value_hint() {
183             ValueHint::Unknown => " -r",
184             // fish has no built-in support to distinguish these
185             ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
186             ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
187             // It seems fish has no built-in support for completing command + arguments as
188             // single string (CommandString). Complete just the command name.
189             ValueHint::CommandString | ValueHint::CommandName => {
190                 " -r -f -a \"(__fish_complete_command)\""
191             }
192             ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
193             ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
194             // Disable completion for others
195             _ => " -r -f",
196         }
197         .to_string()
198     }
199 }
200