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_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
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 => "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 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 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 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 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 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 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;