clap_complete/shells/
fish.rs

1use std::io::Write;
2
3use clap::{builder, Arg, Command, ValueHint};
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 = 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
30fn 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
39fn escape_help(help: &builder::StyledStr) -> String {
40    escape_string(&help.to_string().replace('\n', " "), false)
41}
42
43fn 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                            .flat_map(Command::get_name_and_visible_aliases)
79                            .map(|name| format!("not __fish_seen_subcommand_from {name}"))
80                    )
81                    .collect::<Vec<_>>()
82                    .join("; and ")
83            )
84            .as_str(),
85        );
86    }
87
88    debug!("gen_fish_inner: parent_commands={parent_commands:?}");
89
90    for option in cmd.get_opts() {
91        let mut template = basic_template.clone();
92
93        if let Some(shorts) = option.get_short_and_visible_aliases() {
94            for short in shorts {
95                template.push_str(format!(" -s {short}").as_str());
96            }
97        }
98
99        if let Some(longs) = option.get_long_and_visible_aliases() {
100            for long in longs {
101                template.push_str(format!(" -l {}", escape_string(long, false)).as_str());
102            }
103        }
104
105        if let Some(data) = option.get_help() {
106            template.push_str(&format!(" -d '{}'", escape_help(data)));
107        }
108
109        template.push_str(value_completion(option).as_str());
110
111        buffer.push_str(template.as_str());
112        buffer.push('\n');
113    }
114
115    for flag in utils::flags(cmd) {
116        let mut template = basic_template.clone();
117
118        if let Some(shorts) = flag.get_short_and_visible_aliases() {
119            for short in shorts {
120                template.push_str(format!(" -s {short}").as_str());
121            }
122        }
123
124        if let Some(longs) = flag.get_long_and_visible_aliases() {
125            for long in longs {
126                template.push_str(format!(" -l {}", escape_string(long, false)).as_str());
127            }
128        }
129
130        if let Some(data) = flag.get_help() {
131            template.push_str(&format!(" -d '{}'", escape_help(data)));
132        }
133
134        buffer.push_str(template.as_str());
135        buffer.push('\n');
136    }
137
138    for subcommand in cmd.get_subcommands() {
139        for subcommand_name in subcommand.get_name_and_visible_aliases() {
140            let mut template = basic_template.clone();
141
142            template.push_str(" -f");
143            template.push_str(format!(" -a \"{}\"", subcommand_name).as_str());
144
145            if let Some(data) = subcommand.get_about() {
146                template.push_str(format!(" -d '{}'", escape_help(data)).as_str());
147            }
148
149            buffer.push_str(template.as_str());
150            buffer.push('\n');
151        }
152    }
153
154    // generate options of subcommands
155    for subcommand in cmd.get_subcommands() {
156        for subcommand_name in subcommand.get_name_and_visible_aliases() {
157            let mut parent_commands: Vec<_> = parent_commands.into();
158            parent_commands.push(subcommand_name);
159            gen_fish_inner(root_command, &parent_commands, subcommand, buffer);
160        }
161    }
162}
163
164fn value_completion(option: &Arg) -> String {
165    if !option.get_num_args().expect("built").takes_values() {
166        return "".to_string();
167    }
168
169    if let Some(data) = utils::possible_values(option) {
170        // We return the possible values with their own empty description e.g. {a\t,b\t}
171        // this makes sure that a and b don't get the description of the option or argument
172        format!(
173            " -r -f -a \"{{{}}}\"",
174            data.iter()
175                .filter_map(|value| if value.is_hide_set() {
176                    None
177                } else {
178                    // The help text after \t is wrapped in '' to make sure that the it is taken literally
179                    // and there is no command substitution or variable expansion resulting in unexpected errors
180                    Some(format!(
181                        "{}\t'{}'",
182                        escape_string(value.get_name(), true).as_str(),
183                        escape_help(value.get_help().unwrap_or_default())
184                    ))
185                })
186                .collect::<Vec<_>>()
187                .join(",")
188        )
189    } else {
190        // NB! If you change this, please also update the table in `ValueHint` documentation.
191        match option.get_value_hint() {
192            ValueHint::Unknown => " -r",
193            // fish has no built-in support to distinguish these
194            ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
195            ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
196            // It seems fish has no built-in support for completing command + arguments as
197            // single string (CommandString). Complete just the command name.
198            ValueHint::CommandString | ValueHint::CommandName => {
199                " -r -f -a \"(__fish_complete_command)\""
200            }
201            ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
202            ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
203            // Disable completion for others
204            _ => " -r -f",
205        }
206        .to_string()
207    }
208}