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_auto_cfg))]
20#![forbid(unsafe_code)]
21#![warn(missing_docs)]
22#![warn(clippy::print_stderr)]
23#![warn(clippy::print_stdout)]
24
25use clap::builder::StyledStr;
26use clap::ValueHint;
27use clap::{builder::PossibleValue, Arg, ArgAction, Command};
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 => "path",
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 matches!(arg.get_action(), ArgAction::Append) {
140            s.push_str(format!("    ...{}", arg.get_id()).as_str());
141        } else {
142            s.push_str(format!("    {}", arg.get_id()).as_str());
143
144            if !arg.is_required_set() {
145                s.push('?');
146            }
147        }
148
149        append_value_completion_and_help(arg, name, &possible_values, s);
150
151        return;
152    }
153
154    let shorts = arg.get_short_and_visible_aliases();
155    let longs = arg.get_long_and_visible_aliases();
156
157    match shorts {
158        Some(shorts) => match longs {
159            Some(longs) => {
160                // short options and long options
161                s.push_str(
162                    format!(
163                        "    --{}(-{})",
164                        longs.first().expect("At least one long option expected"),
165                        shorts.first().expect("At lease one short option expected")
166                    )
167                    .as_str(),
168                );
169                append_value_completion_and_help(arg, name, &possible_values, s);
170
171                // long alias
172                for long in longs.iter().skip(1) {
173                    s.push_str(format!("    --{long}").as_str());
174                    append_value_completion_and_help(arg, name, &possible_values, s);
175                }
176
177                // short alias
178                for short in shorts.iter().skip(1) {
179                    s.push_str(format!("    -{short}").as_str());
180                    append_value_completion_and_help(arg, name, &possible_values, s);
181                }
182            }
183            None => {
184                // short options only
185                for short in shorts {
186                    s.push_str(format!("    -{short}").as_str());
187                    append_value_completion_and_help(arg, name, &possible_values, s);
188                }
189            }
190        },
191        None => match longs {
192            Some(longs) => {
193                // long options only
194                for long in longs {
195                    s.push_str(format!("    --{long}").as_str());
196                    append_value_completion_and_help(arg, name, &possible_values, s);
197                }
198            }
199            None => unreachable!("No short or long options found"),
200        },
201    }
202}
203
204fn generate_completion(completions: &mut String, cmd: &Command, is_subcommand: bool) {
205    let name = cmd.get_bin_name().expect("Failed to get bin name");
206
207    for arg in cmd.get_arguments() {
208        append_value_completion_defs(arg, name, completions);
209    }
210
211    if let Some(about) = cmd.get_about() {
212        let about = single_line_styled_str(about);
213        completions.push_str(format!("  # {about}\n").as_str());
214    }
215
216    if is_subcommand {
217        completions.push_str(format!("  export extern \"{name}\" [\n").as_str());
218    } else {
219        completions.push_str(format!("  export extern {name} [\n").as_str());
220    }
221
222    for arg in cmd.get_arguments() {
223        append_argument(arg, name, completions);
224    }
225
226    completions.push_str("  ]\n\n");
227
228    if is_subcommand {
229        for sub in cmd.get_subcommands() {
230            generate_completion(completions, sub, true);
231        }
232    }
233}
234
235fn single_line_styled_str(text: &StyledStr) -> String {
236    text.to_string().replace('\n', " ")
237}
238
239#[doc = include_str!("../README.md")]
240#[cfg(doctest)]
241pub struct ReadmeDoctests;