Skip to main content

clap_complete_nushell/
lib.rs

1//! Generates [Nushell](https://github.com/nushell/nushell) completions for [`clap`](https://github.com/clap-rs/clap) based CLIs
2//!
3//! ## Example
4//!
5//! ```
6//! use clap::Command;
7//! use clap_complete::generate;
8//! use clap_complete_nushell::Nushell;
9//! use std::io;
10//!
11//! let mut cmd = Command::new("myapp")
12//!     .subcommand(Command::new("test").subcommand(Command::new("config")))
13//!     .subcommand(Command::new("hello"));
14//!
15//! generate(Nushell, &mut cmd, "myapp", &mut io::stdout());
16//! ```
17
18#![doc(html_logo_url = "https://raw.githubusercontent.com/clap-rs/clap/master/assets/clap.png")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![forbid(unsafe_code)]
21#![warn(missing_docs)]
22#![warn(clippy::print_stderr)]
23#![warn(clippy::print_stdout)]
24
25use clap::ValueHint;
26use clap::builder::StyledStr;
27use clap::{Arg, ArgAction, Command, builder::PossibleValue};
28use clap_complete::Generator;
29
30/// Generate Nushell complete file
31pub struct Nushell;
32
33impl Generator for Nushell {
34    fn file_name(&self, name: &str) -> String {
35        format!("{name}.nu")
36    }
37
38    fn generate(&self, cmd: &Command, buf: &mut dyn std::io::Write) {
39        self.try_generate(cmd, buf)
40            .expect("failed to write completion file");
41    }
42
43    fn try_generate(
44        &self,
45        cmd: &Command,
46        buf: &mut dyn std::io::Write,
47    ) -> Result<(), std::io::Error> {
48        let mut completions = String::new();
49
50        completions.push_str("module completions {\n\n");
51
52        generate_completion(&mut completions, cmd, false);
53
54        for sub in cmd.get_subcommands() {
55            generate_completion(&mut completions, sub, true);
56        }
57
58        completions.push_str("}\n\n");
59        completions.push_str("export use completions *\n");
60
61        buf.write_all(completions.as_bytes())
62    }
63}
64
65fn append_value_completion_and_help(
66    arg: &Arg,
67    name: &str,
68    possible_values: &[PossibleValue],
69    s: &mut String,
70) {
71    let takes_values = arg
72        .get_num_args()
73        .map(|r| r.takes_values())
74        .unwrap_or(false);
75
76    if takes_values {
77        let nu_type = match arg.get_value_hint() {
78            ValueHint::Unknown => "string",
79            ValueHint::Other => "string",
80            ValueHint::AnyPath => "path",
81            ValueHint::FilePath => "path",
82            ValueHint::DirPath => "directory",
83            ValueHint::ExecutablePath => "path",
84            ValueHint::CommandName => "string",
85            ValueHint::CommandString => "string",
86            ValueHint::CommandWithArguments => "string",
87            ValueHint::Username => "string",
88            ValueHint::Hostname => "string",
89            ValueHint::Url => "string",
90            ValueHint::EmailAddress => "string",
91            _ => "string",
92        };
93        s.push_str(format!(": {nu_type}").as_str());
94
95        if !possible_values.is_empty() {
96            s.push_str(format!(r#"@"nu-complete {} {}""#, name, arg.get_id()).as_str());
97        }
98    }
99
100    if let Some(help) = arg.get_help() {
101        let indent: usize = 30;
102        let width = match s.lines().last() {
103            Some(line) => indent.saturating_sub(line.len()),
104            None => 0,
105        };
106
107        s.push_str(format!("{:>width$}# {}", ' ', single_line_styled_str(help)).as_str());
108    }
109
110    s.push('\n');
111}
112
113fn append_value_completion_defs(arg: &Arg, name: &str, s: &mut String) {
114    let possible_values = arg.get_possible_values();
115    if possible_values.is_empty() {
116        return;
117    }
118
119    s.push_str(format!(r#"  def "nu-complete {} {}" [] {{"#, name, arg.get_id()).as_str());
120    s.push_str("\n    [");
121
122    for value in possible_values {
123        let vname = value.get_name();
124        if vname.contains(|c: char| c.is_whitespace()) {
125            s.push_str(format!(r#" "\"{vname}\"""#).as_str());
126        } else {
127            s.push_str(format!(r#" "{vname}""#).as_str());
128        }
129    }
130
131    s.push_str(" ]\n  }\n\n");
132}
133
134fn append_argument(arg: &Arg, name: &str, s: &mut String) {
135    let possible_values = arg.get_possible_values();
136
137    if arg.is_positional() {
138        // rest arguments
139        if arg.is_last_set() {
140            // skipped, as it's not currently supported by nushell
141            return;
142        }
143
144        if matches!(arg.get_action(), ArgAction::Append) {
145            s.push_str(format!("    ...{}", arg.get_id()).as_str());
146        } else {
147            s.push_str(format!("    {}", arg.get_id()).as_str());
148
149            if !arg.is_required_set() {
150                s.push('?');
151            }
152        }
153
154        append_value_completion_and_help(arg, name, &possible_values, s);
155
156        return;
157    }
158
159    let shorts = arg.get_short_and_visible_aliases();
160    let longs = arg.get_long_and_visible_aliases();
161
162    match shorts {
163        Some(shorts) => match longs {
164            Some(longs) => {
165                // short options and long options
166                s.push_str(
167                    format!(
168                        "    --{}(-{})",
169                        longs.first().expect("At least one long option expected"),
170                        shorts.first().expect("At lease one short option expected")
171                    )
172                    .as_str(),
173                );
174                append_value_completion_and_help(arg, name, &possible_values, s);
175
176                // long alias
177                for long in longs.iter().skip(1) {
178                    s.push_str(format!("    --{long}").as_str());
179                    append_value_completion_and_help(arg, name, &possible_values, s);
180                }
181
182                // short alias
183                for short in shorts.iter().skip(1) {
184                    s.push_str(format!("    -{short}").as_str());
185                    append_value_completion_and_help(arg, name, &possible_values, s);
186                }
187            }
188            None => {
189                // short options only
190                for short in shorts {
191                    s.push_str(format!("    -{short}").as_str());
192                    append_value_completion_and_help(arg, name, &possible_values, s);
193                }
194            }
195        },
196        None => match longs {
197            Some(longs) => {
198                // long options only
199                for long in longs {
200                    s.push_str(format!("    --{long}").as_str());
201                    append_value_completion_and_help(arg, name, &possible_values, s);
202                }
203            }
204            None => unreachable!("No short or long options found"),
205        },
206    }
207}
208
209fn generate_completion(completions: &mut String, cmd: &Command, is_subcommand: bool) {
210    let name = cmd.get_bin_name().expect("Failed to get bin name");
211
212    for arg in cmd.get_arguments() {
213        if arg.is_last_set() {
214            // skipped, as it's not currently supported by nushell
215            continue;
216        }
217
218        append_value_completion_defs(arg, name, completions);
219    }
220
221    if let Some(about) = cmd.get_about() {
222        let about = single_line_styled_str(about);
223        completions.push_str(format!("  # {about}\n").as_str());
224    }
225
226    if is_subcommand {
227        completions.push_str(format!("  export extern \"{name}\" [\n").as_str());
228    } else {
229        completions.push_str(format!("  export extern {name} [\n").as_str());
230    }
231
232    let flags: Vec<_> = cmd.get_arguments().filter(|a| !a.is_positional()).collect();
233    let mut positionals: Vec<_> = cmd.get_positionals().collect();
234
235    positionals.sort_by_key(|arg| arg.get_index());
236
237    for arg in flags {
238        append_argument(arg, name, completions);
239    }
240
241    for arg in positionals {
242        append_argument(arg, name, completions);
243    }
244
245    completions.push_str("  ]\n\n");
246
247    if is_subcommand {
248        for sub in cmd.get_subcommands() {
249            generate_completion(completions, sub, true);
250        }
251    }
252}
253
254fn single_line_styled_str(text: &StyledStr) -> String {
255    text.to_string().replace('\n', " ")
256}
257
258#[doc = include_str!("../README.md")]
259#[cfg(doctest)]
260pub struct ReadmeDoctests;