1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Std
use std::io::Write;

// Internal
use crate::Generator;
use clap::*;

/// Generate fish completion file
///
/// Note: The fish generator currently only supports named options (-o/--option), not positional arguments.
pub struct Fish;

impl Generator for Fish {
    fn file_name(name: &str) -> String {
        format!("{}.fish", name)
    }

    fn generate(app: &App, buf: &mut dyn Write) {
        let command = app.get_bin_name().unwrap();
        let mut buffer = String::new();

        gen_fish_inner(command, app, &mut buffer);
        w!(buf, buffer.as_bytes());
    }
}

// Escape string inside single quotes
fn escape_string(string: &str) -> String {
    string.replace("\\", "\\\\").replace("'", "\\'")
}

fn gen_fish_inner(root_command: &str, app: &App, buffer: &mut String) {
    debug!("gen_fish_inner");
    // example :
    //
    // complete
    //      -c {command}
    //      -d "{description}"
    //      -s {short}
    //      -l {long}
    //      -a "{possible_arguments}"
    //      -r # if require parameter
    //      -f # don't use file completion
    //      -n "__fish_use_subcommand"               # complete for command "myprog"
    //      -n "__fish_seen_subcommand_from subcmd1" # complete for command "myprog subcmd1"

    let mut basic_template = format!("complete -c {}", root_command);
    let mut bin_name = app.get_bin_name().unwrap();

    if root_command == bin_name {
        if app.has_subcommands() {
            basic_template.push_str(" -n \"__fish_use_subcommand\"");
        }
    } else {
        bin_name = app.get_name();
        basic_template
            .push_str(format!(" -n \"__fish_seen_subcommand_from {}\"", bin_name).as_str());
    }

    debug!("gen_fish_inner: bin_name={}", bin_name);

    for option in app.get_opts() {
        let mut template = basic_template.clone();

        if let Some(shorts) = option.get_short_and_visible_aliases() {
            for short in shorts {
                template.push_str(format!(" -s {}", short).as_str());
            }
        }

        if let Some(longs) = option.get_long_and_visible_aliases() {
            for long in longs {
                template.push_str(format!(" -l {}", escape_string(long)).as_str());
            }
        }

        if let Some(data) = option.get_about() {
            template.push_str(format!(" -d '{}'", escape_string(data)).as_str());
        }

        template.push_str(value_completion(option).as_str());

        buffer.push_str(template.as_str());
        buffer.push('\n');
    }

    for flag in Fish::flags(app) {
        let mut template = basic_template.clone();

        if let Some(shorts) = flag.get_short_and_visible_aliases() {
            for short in shorts {
                template.push_str(format!(" -s {}", short).as_str());
            }
        }

        if let Some(longs) = flag.get_long_and_visible_aliases() {
            for long in longs {
                template.push_str(format!(" -l {}", escape_string(long)).as_str());
            }
        }

        if let Some(data) = flag.get_about() {
            template.push_str(format!(" -d '{}'", escape_string(data)).as_str());
        }

        buffer.push_str(template.as_str());
        buffer.push('\n');
    }

    for subcommand in app.get_subcommands() {
        let mut template = basic_template.clone();

        template.push_str(" -f");
        template.push_str(format!(" -a \"{}\"", &subcommand.get_name()).as_str());

        if let Some(data) = subcommand.get_about() {
            template.push_str(format!(" -d '{}'", escape_string(data)).as_str())
        }

        buffer.push_str(template.as_str());
        buffer.push('\n');
    }

    // generate options of subcommands
    for subcommand in app.get_subcommands() {
        gen_fish_inner(root_command, subcommand, buffer);
    }
}

fn value_completion(option: &Arg) -> String {
    if !option.is_set(ArgSettings::TakesValue) {
        return "".to_string();
    }

    if let Some(data) = option.get_possible_values() {
        format!(" -r -f -a \"{}\"", data.join(" "))
    } else {
        // NB! If you change this, please also update the table in `ValueHint` documentation.
        match option.get_value_hint() {
            ValueHint::Unknown => " -r",
            // fish has no built-in support to distinguish these
            ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F",
            ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"",
            // It seems fish has no built-in support for completing command + arguments as
            // single string (CommandString). Complete just the command name.
            ValueHint::CommandString | ValueHint::CommandName => {
                " -r -f -a \"(__fish_complete_command)\""
            }
            ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"",
            ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"",
            // Disable completion for others
            _ => " -r -f",
        }
        .to_string()
    }
}