cli_rs/
parser.rs

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]) // --version
58                .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    // split this out into a trait that is pub, make the rest not pub
122    fn parse(&mut self) {
123        let args: Vec<String> = env::args().collect();
124        // cmd complete shell word_idx [input]
125        if args.len() >= 5 && args[1] == "complete" {
126            let name = self.docs().name.to_string();
127            // going to need some serious tests here
128            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        // recurse into subcommand?
209        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            // todo check this
219            if let Some(index) = subcommand_index {
220                return self.complete_subcommand(index, &tokens[1..]);
221            }
222
223            // print subcommands that begin with the token
224            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                    // in a future where we manage errors more properly, this section could be
248                    // closer to how the parser works, eliminating consumed symbols and helping
249                    // the end user not see completions for flags they've already typed. Presently
250                    // that code would start outputting errors.
251                    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        // try to match subcommands
330        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}