usage/
parse.rs

1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use strum::EnumTryAs;
9
10#[cfg(feature = "docs")]
11use crate::docs;
12use crate::error::UsageErr;
13use crate::{Spec, SpecArg, SpecCommand, SpecFlag};
14
15pub struct ParseOutput {
16    pub cmd: SpecCommand,
17    pub cmds: Vec<SpecCommand>,
18    pub args: IndexMap<SpecArg, ParseValue>,
19    pub flags: IndexMap<SpecFlag, ParseValue>,
20    pub available_flags: BTreeMap<String, SpecFlag>,
21    pub flag_awaiting_value: Vec<SpecFlag>,
22    pub errors: Vec<UsageErr>,
23}
24
25#[derive(Debug, EnumTryAs, Clone)]
26pub enum ParseValue {
27    Bool(bool),
28    String(String),
29    MultiBool(Vec<bool>),
30    MultiString(Vec<String>),
31}
32
33pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
34    let mut out = parse_partial(spec, input)?;
35    trace!("{out:?}");
36    for arg in out.cmd.args.iter().skip(out.args.len()) {
37        if let Some(default) = arg.default.as_ref() {
38            out.args
39                .insert(arg.clone(), ParseValue::String(default.clone()));
40        }
41    }
42    for flag in out.available_flags.values() {
43        if out.flags.contains_key(flag) {
44            continue;
45        }
46        if let Some(default) = flag.default.as_ref() {
47            out.flags
48                .insert(flag.clone(), ParseValue::String(default.clone()));
49        }
50        if let Some(Some(default)) = flag.arg.as_ref().map(|a| &a.default) {
51            out.flags
52                .insert(flag.clone(), ParseValue::String(default.clone()));
53        }
54    }
55    if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
56        bail!("{err}");
57    }
58    if !out.errors.is_empty() {
59        bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
60    }
61    Ok(out)
62}
63
64pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
65    trace!("parse_partial: {input:?}");
66    let mut input = input.iter().cloned().collect::<VecDeque<_>>();
67    input.pop_front();
68
69    let gather_flags = |cmd: &SpecCommand| {
70        cmd.flags
71            .iter()
72            .flat_map(|f| {
73                let mut flags = f
74                    .long
75                    .iter()
76                    .map(|l| (format!("--{}", l), f.clone()))
77                    .chain(f.short.iter().map(|s| (format!("-{}", s), f.clone())))
78                    .collect::<Vec<_>>();
79                if let Some(negate) = &f.negate {
80                    flags.push((negate.clone(), f.clone()));
81                }
82                flags
83            })
84            .collect()
85    };
86
87    let mut out = ParseOutput {
88        cmd: spec.cmd.clone(),
89        cmds: vec![spec.cmd.clone()],
90        args: IndexMap::new(),
91        flags: IndexMap::new(),
92        available_flags: gather_flags(&spec.cmd),
93        flag_awaiting_value: vec![],
94        errors: vec![],
95    };
96
97    while !input.is_empty() {
98        if let Some(subcommand) = out.cmd.find_subcommand(&input[0]) {
99            let mut subcommand = subcommand.clone();
100            subcommand.mount()?;
101            out.available_flags.retain(|_, f| f.global);
102            out.available_flags.extend(gather_flags(&subcommand));
103            input.pop_front();
104            out.cmds.push(subcommand.clone());
105            out.cmd = subcommand.clone();
106        } else {
107            break;
108        }
109    }
110
111    let mut next_arg = out.cmd.args.first();
112    let mut enable_flags = true;
113    let mut grouped_flag = false;
114
115    while !input.is_empty() {
116        let mut w = input.pop_front().unwrap();
117
118        if w == "--" {
119            enable_flags = false;
120            continue;
121        }
122
123        // long flags
124        if enable_flags && w.starts_with("--") {
125            grouped_flag = false;
126            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
127            if !val.is_empty() {
128                input.push_front(val.to_string());
129            }
130            if let Some(f) = out.available_flags.get(word) {
131                if f.arg.is_some() {
132                    out.flag_awaiting_value.push(f.clone());
133                } else if f.var {
134                    let arr = out
135                        .flags
136                        .entry(f.clone())
137                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
138                        .try_as_multi_bool_mut()
139                        .unwrap();
140                    arr.push(true);
141                } else {
142                    let negate = f.negate.clone().unwrap_or_default();
143                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
144                }
145                continue;
146            }
147            if is_help_arg(spec, &w) {
148                out.errors
149                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
150                return Ok(out);
151            }
152        }
153
154        // short flags
155        if enable_flags && w.starts_with('-') && w.len() > 1 {
156            let short = w.chars().nth(1).unwrap();
157            if let Some(f) = out.available_flags.get(&format!("-{}", short)) {
158                if w.len() > 2 {
159                    input.push_front(format!("-{}", &w[2..]));
160                    grouped_flag = true;
161                }
162                if f.arg.is_some() {
163                    out.flag_awaiting_value.push(f.clone());
164                } else if f.var {
165                    let arr = out
166                        .flags
167                        .entry(f.clone())
168                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
169                        .try_as_multi_bool_mut()
170                        .unwrap();
171                    arr.push(true);
172                } else {
173                    let negate = f.negate.clone().unwrap_or_default();
174                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
175                }
176                continue;
177            }
178            if is_help_arg(spec, &w) {
179                out.errors
180                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
181                return Ok(out);
182            }
183            if grouped_flag {
184                grouped_flag = false;
185                w.remove(0);
186            }
187        }
188
189        if !out.flag_awaiting_value.is_empty() {
190            while let Some(flag) = out.flag_awaiting_value.pop() {
191                let arg = flag.arg.as_ref().unwrap();
192                if flag.var {
193                    let arr = out
194                        .flags
195                        .entry(flag)
196                        .or_insert_with(|| ParseValue::MultiString(vec![]))
197                        .try_as_multi_string_mut()
198                        .unwrap();
199                    arr.push(w);
200                } else {
201                    if let Some(choices) = &arg.choices {
202                        if !choices.choices.contains(&w) {
203                            if is_help_arg(spec, &w) {
204                                out.errors
205                                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
206                                return Ok(out);
207                            }
208                            bail!(
209                                "Invalid choice for option {}: {w}, expected one of {}",
210                                flag.name,
211                                choices.choices.join(", ")
212                            );
213                        }
214                    }
215                    out.flags.insert(flag, ParseValue::String(w));
216                }
217                w = "".to_string();
218            }
219            continue;
220        }
221
222        if let Some(arg) = next_arg {
223            if arg.var {
224                let arr = out
225                    .args
226                    .entry(arg.clone())
227                    .or_insert_with(|| ParseValue::MultiString(vec![]))
228                    .try_as_multi_string_mut()
229                    .unwrap();
230                arr.push(w);
231                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
232                    next_arg = out.cmd.args.get(out.args.len());
233                }
234            } else {
235                if let Some(choices) = &arg.choices {
236                    if !choices.choices.contains(&w) {
237                        if is_help_arg(spec, &w) {
238                            out.errors
239                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
240                            return Ok(out);
241                        }
242                        bail!(
243                            "Invalid choice for arg {}: {w}, expected one of {}",
244                            arg.name,
245                            choices.choices.join(", ")
246                        );
247                    }
248                }
249                out.args.insert(arg.clone(), ParseValue::String(w));
250                next_arg = out.cmd.args.get(out.args.len());
251            }
252            continue;
253        }
254        if is_help_arg(spec, &w) {
255            out.errors
256                .push(render_help_err(spec, &out.cmd, w.len() > 2));
257            return Ok(out);
258        }
259        bail!("unexpected word: {w}");
260    }
261
262    for arg in out.cmd.args.iter().skip(out.args.len()) {
263        if arg.required && arg.default.is_none() {
264            out.errors.push(UsageErr::MissingArg(arg.name.clone()));
265        }
266    }
267
268    for flag in out.available_flags.values() {
269        if out.flags.contains_key(flag) {
270            continue;
271        }
272        let has_default = flag.default.is_some() || flag.arg.iter().any(|a| a.default.is_some());
273        if flag.required && !has_default {
274            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
275        }
276    }
277
278    Ok(out)
279}
280
281#[cfg(feature = "docs")]
282fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
283    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
284}
285
286#[cfg(not(feature = "docs"))]
287fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
288    UsageErr::Help("help".to_string())
289}
290
291fn is_help_arg(spec: &Spec, w: &str) -> bool {
292    spec.disable_help != Some(true)
293        && (w == "--help"
294            || w == "-h"
295            || w == "-?"
296            || (spec.cmd.subcommands.is_empty() && w == "help"))
297}
298
299impl ParseOutput {
300    pub fn as_env(&self) -> BTreeMap<String, String> {
301        let mut env = BTreeMap::new();
302        for (flag, val) in &self.flags {
303            let key = format!("usage_{}", flag.name.to_snake_case());
304            let val = match val {
305                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
306                ParseValue::String(s) => s.clone(),
307                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
308                ParseValue::MultiString(s) => shell_words::join(s),
309            };
310            env.insert(key, val);
311        }
312        for (arg, val) in &self.args {
313            let key = format!("usage_{}", arg.name.to_snake_case());
314            env.insert(key, val.to_string());
315        }
316        env
317    }
318}
319
320impl Display for ParseValue {
321    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
322        match self {
323            ParseValue::Bool(b) => write!(f, "{}", b),
324            ParseValue::String(s) => write!(f, "{}", s),
325            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
326            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
327        }
328    }
329}
330
331impl Debug for ParseOutput {
332    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
333        f.debug_struct("ParseOutput")
334            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
335            .field(
336                "args",
337                &self
338                    .args
339                    .iter()
340                    .map(|(a, w)| format!("{}: {w}", &a.name))
341                    .collect_vec(),
342            )
343            .field(
344                "available_flags",
345                &self
346                    .available_flags
347                    .iter()
348                    .map(|(f, w)| format!("{f}: {w}"))
349                    .collect_vec(),
350            )
351            .field(
352                "flags",
353                &self
354                    .flags
355                    .iter()
356                    .map(|(f, w)| format!("{}: {w}", &f.name))
357                    .collect_vec(),
358            )
359            .field("flag_awaiting_value", &self.flag_awaiting_value)
360            .field("errors", &self.errors)
361            .finish()
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_parse() {
371        let mut cmd = SpecCommand::default();
372        cmd.name = "test".to_string();
373        cmd.args = vec![SpecArg {
374            name: "arg".to_string(),
375            ..Default::default()
376        }];
377        cmd.flags = vec![SpecFlag {
378            name: "flag".to_string(),
379            long: vec!["flag".to_string()],
380            ..Default::default()
381        }];
382        let spec = Spec {
383            name: "test".to_string(),
384            bin: "test".to_string(),
385            cmd,
386            ..Default::default()
387        };
388        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
389        let parsed = parse(&spec, &input).unwrap();
390        assert_eq!(parsed.cmds.len(), 1);
391        assert_eq!(parsed.cmds[0].name, "test");
392        assert_eq!(parsed.args.len(), 1);
393        assert_eq!(parsed.flags.len(), 1);
394        assert_eq!(parsed.available_flags.len(), 1);
395    }
396
397    #[test]
398    fn test_as_env() {
399        let mut cmd = SpecCommand::default();
400        cmd.name = "test".to_string();
401        cmd.args = vec![SpecArg {
402            name: "arg".to_string(),
403            ..Default::default()
404        }];
405        cmd.flags = vec![
406            SpecFlag {
407                name: "flag".to_string(),
408                long: vec!["flag".to_string()],
409                ..Default::default()
410            },
411            SpecFlag {
412                name: "force".to_string(),
413                long: vec!["force".to_string()],
414                negate: Some("--no-force".to_string()),
415                ..Default::default()
416            },
417        ];
418        let spec = Spec {
419            name: "test".to_string(),
420            bin: "test".to_string(),
421            cmd,
422            ..Default::default()
423        };
424        let input = vec![
425            "test".to_string(),
426            "--flag".to_string(),
427            "--no-force".to_string(),
428        ];
429        let parsed = parse(&spec, &input).unwrap();
430        let env = parsed.as_env();
431        assert_eq!(env.len(), 2);
432        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
433        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
434    }
435}