clap_complete_nushell/
lib.rs1#![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
30pub 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 if arg.is_last_set() {
140 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 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 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 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 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 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 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;