1use std::{env, fmt::Write};
2
3use crate::{
4 cli_error::{CliError, CliResult, Exit},
5 command::{CompletionMode, ParserInfo},
6 flag::Flag,
7 input::{Input, InputType},
8};
9
10use colored::*;
11
12impl<C> Cmd for C where C: ParserInfo {}
13
14pub struct CompOut {
15 pub name: String,
16 pub desc: Option<String>,
17}
18
19fn version_flag() -> Flag<'static, bool> {
20 Flag::bool("version").description("display CLI version")
21}
22
23fn help_flag() -> Flag<'static, bool> {
24 Flag::bool("help").description("view help")
25}
26
27pub trait Cmd: ParserInfo {
28 fn gen_help(&mut self) -> CliError {
29 let cmd_path = self.docs().cmd_path();
30 let mut help_message = String::new();
31
32 write!(help_message, "{}", cmd_path.bold().green()).unwrap();
33
34 if let Some(description) = &self.docs().description {
35 write!(help_message, " - {description}").unwrap();
36 }
37
38 writeln!(help_message, "\n").unwrap();
39
40 let mut version = version_flag();
41 let mut help = help_flag();
42 let mut built_in: Vec<&mut dyn Input> = vec![&mut help];
43 if self.docs().version.is_some() {
44 built_in.push(&mut version);
45 }
46 let subcommands = self.subcommand_docs();
47 writeln!(help_message, "{}", "USAGE:".bold().yellow()).unwrap();
48 if subcommands.is_empty() {
49 let usage = format!("{cmd_path} [options], <args>").bold();
50 writeln!(help_message, "\t{usage}").unwrap();
51 writeln!(help_message, "\n{}", "FLAGS:".yellow().bold()).unwrap();
52
53 let width = self
54 .symbols()
55 .into_iter()
56 .map(|s| s.display_name().len() + 3)
57 .chain([10]) .max()
59 .unwrap();
60
61 for symbol in self.symbols().iter().chain(built_in.iter()) {
62 if symbol.type_name() == InputType::Flag {
63 write!(help_message, "\t--{:width$}", symbol.display_name().bold()).unwrap();
64 if let Some(desc) = symbol.description() {
65 write!(help_message, " {desc}").unwrap();
66 }
67 writeln!(help_message).unwrap();
68 }
69 }
70 writeln!(help_message, "\n{}", "ARGS:".yellow().bold()).unwrap();
71 for symbol in self.symbols() {
72 if symbol.type_name() == InputType::Arg {
73 write!(help_message, "\t{:width$}", symbol.display_name().bold()).unwrap();
74 if let Some(desc) = symbol.description() {
75 write!(help_message, " {desc}").unwrap();
76 }
77 writeln!(help_message).unwrap();
78 }
79 }
80 } else {
81 let usage = format! {"{cmd_path} <subcommand>"}.bold();
82 writeln!(help_message, "\t{usage}").unwrap();
83 writeln!(help_message, "\n{}", "SUBCOMMANDS:".yellow().bold()).unwrap();
84 let sub_width = subcommands.iter().map(|s| s.name.len()).max().unwrap();
85 for subcommand in subcommands {
86 write!(help_message, "\t{:sub_width$}", subcommand.name.bold()).unwrap();
87 if let Some(description) = subcommand.description {
88 write!(help_message, " {description}").unwrap();
89 }
90
91 writeln!(help_message).unwrap();
92 }
93
94 writeln!(help_message, "\n{}", "FLAGS:".yellow().bold()).unwrap();
95
96 let flag_width = built_in
97 .iter()
98 .map(|s| s.display_name().len() + 3)
99 .max()
100 .unwrap();
101
102 for symbol in built_in {
103 if symbol.type_name() == InputType::Flag {
104 write!(
105 help_message,
106 "\t--{:flag_width$}",
107 symbol.display_name().bold()
108 )
109 .unwrap();
110 if let Some(desc) = symbol.description() {
111 write!(help_message, " {desc}").unwrap();
112 }
113 writeln!(help_message).unwrap();
114 }
115 }
116 }
117
118 CliError::from(help_message)
119 }
120
121 fn parse(&mut self) {
123 let args: Vec<String> = env::args().collect();
124 if args.len() >= 5 && args[1] == "complete" {
126 let name = self.docs().name.to_string();
127 let shell = args[2].parse::<CompletionMode>().unwrap();
129 let prompt: Vec<String> = if shell == CompletionMode::Fish {
130 let prompt = &args[4];
131 prompt.split(' ').map(|s| s.to_string()).collect()
132 } else {
133 let idx: usize = args[3].parse().unwrap();
134 let prompt = &args[4];
135 prompt
136 .split(' ')
137 .map(|s| s.to_string())
138 .take(idx + 1)
139 .collect()
140 };
141
142 let mut last_command_location = 0;
143
144 for (i, token) in prompt.iter().enumerate().rev() {
145 if token == &name {
146 last_command_location = i;
147 break;
148 }
149 }
150
151 let prompt = &prompt[last_command_location..];
152
153 match self.complete_args(&prompt[1..]) {
154 Ok(outputs) => {
155 match shell {
156 CompletionMode::Bash => {
157 for out in outputs {
158 println!("{}", out.name);
159 }
160 }
161 CompletionMode::Fish => {
162 for out in outputs {
163 if let Some(desc) = out.desc {
164 println!("{}\t{}", out.name, desc);
165 } else {
166 println!("{}", out.name);
167 }
168 }
169 }
170 CompletionMode::Zsh => {
171 let comps = outputs
172 .into_iter()
173 .map(|out| {
174 if let Some(desc) = out.desc {
175 let desc = desc.replace('\'', "");
176 let desc = desc.replace('"', "");
177 format!("'{}:{}'", out.name, desc)
178 } else {
179 format!("'{}'", out.name)
180 }
181 })
182 .collect::<Vec<String>>()
183 .join(" ");
184
185 println!("_describe '{name}' \"({comps})\"");
186 }
187 };
188
189 std::process::exit(0);
190 }
191 Err(error) => {
192 std::process::exit(error.status);
193 }
194 }
195 }
196
197 self.parse_args(&args[1..]).exit();
198 }
199
200 fn complete_args(&mut self, tokens: &[String]) -> CliResult<Vec<CompOut>> {
201 let mut completions = vec![];
202 if tokens.is_empty() {
203 return Ok(completions);
204 }
205
206 let subcommands = self.subcommand_docs();
207
208 if !subcommands.is_empty() && !tokens.is_empty() {
210 let token = &tokens[0];
211 let mut subcommand_index = None;
212 for (idx, subcommand) in subcommands.iter().enumerate() {
213 if &subcommand.name == token {
214 subcommand_index = Some(idx);
215 }
216 }
217
218 if let Some(index) = subcommand_index {
220 return self.complete_subcommand(index, &tokens[1..]);
221 }
222
223 if tokens.len() == 1 && !tokens[0].starts_with('-') {
225 for sub in subcommands {
226 if sub.name.starts_with(token) {
227 let name = &sub.name;
228 let desc = &sub.description;
229 completions.push(CompOut {
230 name: name.to_string(),
231 desc: desc.to_owned(),
232 })
233 }
234 }
235
236 return Ok(completions);
237 }
238 }
239
240 let has_version = self.docs().version.is_some();
241 let mut symbols = self.symbols();
242
243 let mut positional_args_so_far = 0;
244 if tokens.len() > 1 {
245 for token in &tokens[0..tokens.len() - 1] {
246 if !token.starts_with('-') {
247 positional_args_so_far += 1;
252 }
253 }
254 }
255
256 let token = &tokens[tokens.len() - 1];
257 if let Some(mut completion_token) = token.strip_prefix('-') {
258 if let Some(second_dash_removed) = completion_token.strip_prefix('-') {
259 completion_token = second_dash_removed;
260 }
261 let value_completion = completion_token.split('=').collect::<Vec<&str>>();
262 if value_completion.len() > 1 {
263 for symbol in &mut symbols {
264 if symbol.display_name() == value_completion[0] {
265 for completion in symbol.complete(value_completion[1])? {
266 completions.push(CompOut {
267 name: format!("--{}={completion}", symbol.display_name()),
268 desc: None,
269 });
270 }
271 return Ok(completions);
272 }
273 }
274 }
275
276 let mut version = version_flag();
277 let mut help = help_flag();
278 let mut built_in: Vec<&mut dyn Input> = vec![&mut help];
279 if has_version {
280 built_in.push(&mut version);
281 }
282
283 symbols
284 .iter()
285 .chain(built_in.iter())
286 .filter(|sym| sym.type_name() == InputType::Flag)
287 .filter(|sym| sym.display_name().starts_with(completion_token))
288 .for_each(|flag| {
289 if flag.is_bool_flag() {
290 completions.push(CompOut {
291 name: format!("--{}", flag.display_name()),
292 desc: flag.description(),
293 });
294 } else {
295 completions.push(CompOut {
296 name: format!("--{}=", flag.display_name()),
297 desc: flag.description(),
298 });
299 }
300 });
301 } else {
302 let arg = symbols
303 .iter_mut()
304 .filter(|sym| sym.type_name() == InputType::Arg)
305 .nth(positional_args_so_far);
306
307 if let Some(arg) = arg {
308 for option in arg.complete(token)? {
309 completions.push(CompOut {
310 name: option.to_string(),
311 desc: None,
312 });
313 }
314 }
315 }
316
317 Ok(completions)
318 }
319
320 fn parse_args(&mut self, tokens: &[String]) -> CliResult<()> {
321 let subcommands = self.subcommand_docs();
322 let symbols = self.symbols();
323 let required_args = symbols.iter().filter(|f| !f.has_default()).count();
324
325 if tokens.is_empty() && (required_args > 0 || !subcommands.is_empty()) {
326 return Err(self.gen_help());
327 }
328
329 if !tokens.is_empty() {
331 let token = &tokens[0];
332 if token == "--help" {
333 println!("{}", self.gen_help().msg);
334 return Ok(());
335 }
336
337 if token == "--version" {
338 let docs = &self.docs();
339 if let Some(version) = &docs.version {
340 println!("{} -- {}", docs.cmd_path(), version);
341 return Ok(());
342 }
343 }
344 if !subcommands.is_empty() {
345 for (idx, subcommand) in subcommands.iter().enumerate() {
346 if &subcommand.name == token {
347 return self.parse_subcommand(idx, &tokens[1..]);
348 }
349 }
350
351 return Err(CliError::from(format!("{token} is not a valid subcommand")));
352 }
353 }
354
355 let mut symbols = self.symbols();
356
357 for token in tokens {
358 if token.starts_with('-') {
359 let mut token_matched = false;
360 for symbol in &mut symbols {
361 if !symbol.parsed() && symbol.type_name() == InputType::Flag {
362 let consumed = symbol.parse(token)?;
363 if consumed {
364 token_matched = true;
365 }
366 }
367 }
368 if !token_matched {
369 return Err(CliError::from(format!(
370 "Unexpected flag-like token found {token}"
371 )));
372 }
373 } else {
374 'args: for symbol in &mut symbols {
375 if !symbol.parsed() && symbol.type_name() == InputType::Arg {
376 symbol.parse(token)?;
377 break 'args;
378 }
379 }
380 }
381 }
382
383 for symbol in symbols {
384 if symbol.type_name() == InputType::Arg && !symbol.has_default() && !symbol.parsed() {
385 return Err(CliError::from(format!(
386 "Missing required argument: {}",
387 symbol.display_name()
388 )));
389 }
390 }
391
392 self.call_handler()
393 }
394}