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 = 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 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                    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