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: Option<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: None,
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
114    while !input.is_empty() {
115        let w = input.pop_front().unwrap();
116
117        if let Some(flag) = out.flag_awaiting_value {
118            out.flag_awaiting_value = None;
119            let arg = flag.arg.as_ref().unwrap();
120            if flag.var {
121                let arr = out
122                    .flags
123                    .entry(flag)
124                    .or_insert_with(|| ParseValue::MultiString(vec![]))
125                    .try_as_multi_string_mut()
126                    .unwrap();
127                arr.push(w);
128            } else {
129                if let Some(choices) = &arg.choices {
130                    if !choices.choices.contains(&w) {
131                        if is_help_arg(spec, &w) {
132                            out.errors
133                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
134                            return Ok(out);
135                        }
136                        bail!(
137                            "Invalid choice for option {}: {w}, expected one of {}",
138                            flag.name,
139                            choices.choices.join(", ")
140                        );
141                    }
142                }
143                out.flags.insert(flag, ParseValue::String(w));
144            }
145            continue;
146        }
147
148        if w == "--" {
149            enable_flags = false;
150            continue;
151        }
152
153        // long flags
154        if enable_flags && w.starts_with("--") {
155            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
156            if !val.is_empty() {
157                input.push_front(val.to_string());
158            }
159            if let Some(f) = out.available_flags.get(word) {
160                if f.arg.is_some() {
161                    out.flag_awaiting_value = Some(f.clone());
162                } else if f.var {
163                    let arr = out
164                        .flags
165                        .entry(f.clone())
166                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
167                        .try_as_multi_bool_mut()
168                        .unwrap();
169                    arr.push(true);
170                } else {
171                    let negate = f.negate.clone().unwrap_or_default();
172                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
173                }
174                continue;
175            }
176            if is_help_arg(spec, &w) {
177                out.errors
178                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
179                return Ok(out);
180            }
181        }
182
183        // short flags
184        if enable_flags && w.starts_with('-') && w.len() > 1 {
185            let short = w.chars().nth(1).unwrap();
186            if let Some(f) = out.available_flags.get(&format!("-{}", short)) {
187                if w.len() > 2 {
188                    input.push_front(w[2..].to_string());
189                }
190                if f.arg.is_some() {
191                    out.flag_awaiting_value = Some(f.clone());
192                } else if f.var {
193                    let arr = out
194                        .flags
195                        .entry(f.clone())
196                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
197                        .try_as_multi_bool_mut()
198                        .unwrap();
199                    arr.push(true);
200                } else {
201                    let negate = f.negate.clone().unwrap_or_default();
202                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
203                }
204                continue;
205            }
206            if is_help_arg(spec, &w) {
207                out.errors
208                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
209                return Ok(out);
210            }
211        }
212
213        if let Some(arg) = next_arg {
214            if arg.var {
215                let arr = out
216                    .args
217                    .entry(arg.clone())
218                    .or_insert_with(|| ParseValue::MultiString(vec![]))
219                    .try_as_multi_string_mut()
220                    .unwrap();
221                arr.push(w);
222                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
223                    next_arg = out.cmd.args.get(out.args.len());
224                }
225            } else {
226                if let Some(choices) = &arg.choices {
227                    if !choices.choices.contains(&w) {
228                        if is_help_arg(spec, &w) {
229                            out.errors
230                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
231                            return Ok(out);
232                        }
233                        bail!(
234                            "Invalid choice for arg {}: {w}, expected one of {}",
235                            arg.name,
236                            choices.choices.join(", ")
237                        );
238                    }
239                }
240                out.args.insert(arg.clone(), ParseValue::String(w));
241                next_arg = out.cmd.args.get(out.args.len());
242            }
243            continue;
244        }
245        if is_help_arg(spec, &w) {
246            out.errors
247                .push(render_help_err(spec, &out.cmd, w.len() > 2));
248            return Ok(out);
249        }
250        bail!("unexpected word: {w}");
251    }
252
253    for arg in out.cmd.args.iter().skip(out.args.len()) {
254        if arg.required && arg.default.is_none() {
255            out.errors.push(UsageErr::MissingArg(arg.name.clone()));
256        }
257    }
258
259    for flag in out.available_flags.values() {
260        if out.flags.contains_key(flag) {
261            continue;
262        }
263        let has_default = flag.default.is_some() || flag.arg.iter().any(|a| a.default.is_some());
264        if flag.required && !has_default {
265            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
266        }
267    }
268
269    Ok(out)
270}
271
272#[cfg(feature = "docs")]
273fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
274    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
275}
276
277#[cfg(not(feature = "docs"))]
278fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
279    UsageErr::Help("help".to_string())
280}
281
282fn is_help_arg(spec: &Spec, w: &str) -> bool {
283    spec.disable_help != Some(true)
284        && (w == "--help"
285            || w == "-h"
286            || w == "-?"
287            || (spec.cmd.subcommands.is_empty() && w == "help"))
288}
289
290impl ParseOutput {
291    pub fn as_env(&self) -> BTreeMap<String, String> {
292        let mut env = BTreeMap::new();
293        for (flag, val) in &self.flags {
294            let key = format!("usage_{}", flag.name.to_snake_case());
295            let val = match val {
296                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
297                ParseValue::String(s) => s.clone(),
298                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
299                ParseValue::MultiString(s) => shell_words::join(s),
300            };
301            env.insert(key, val);
302        }
303        for (arg, val) in &self.args {
304            let key = format!("usage_{}", arg.name.to_snake_case());
305            env.insert(key, val.to_string());
306        }
307        env
308    }
309}
310
311impl Display for ParseValue {
312    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
313        match self {
314            ParseValue::Bool(b) => write!(f, "{}", b),
315            ParseValue::String(s) => write!(f, "{}", s),
316            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
317            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
318        }
319    }
320}
321
322impl Debug for ParseOutput {
323    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
324        f.debug_struct("ParseOutput")
325            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
326            .field(
327                "args",
328                &self
329                    .args
330                    .iter()
331                    .map(|(a, w)| format!("{}: {w}", &a.name))
332                    .collect_vec(),
333            )
334            .field(
335                "available_flags",
336                &self
337                    .available_flags
338                    .iter()
339                    .map(|(f, w)| format!("{f}: {w}"))
340                    .collect_vec(),
341            )
342            .field(
343                "flags",
344                &self
345                    .flags
346                    .iter()
347                    .map(|(f, w)| format!("{}: {w}", &f.name))
348                    .collect_vec(),
349            )
350            .field("flag_awaiting_value", &self.flag_awaiting_value)
351            .field("errors", &self.errors)
352            .finish()
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_parse() {
362        let mut cmd = SpecCommand::default();
363        cmd.name = "test".to_string();
364        cmd.args = vec![SpecArg {
365            name: "arg".to_string(),
366            ..Default::default()
367        }];
368        cmd.flags = vec![SpecFlag {
369            name: "flag".to_string(),
370            long: vec!["flag".to_string()],
371            ..Default::default()
372        }];
373        let spec = Spec {
374            name: "test".to_string(),
375            bin: "test".to_string(),
376            cmd,
377            ..Default::default()
378        };
379        let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
380        let parsed = parse(&spec, &input).unwrap();
381        assert_eq!(parsed.cmds.len(), 1);
382        assert_eq!(parsed.cmds[0].name, "test");
383        assert_eq!(parsed.args.len(), 1);
384        assert_eq!(parsed.flags.len(), 1);
385        assert_eq!(parsed.available_flags.len(), 1);
386    }
387
388    #[test]
389    fn test_as_env() {
390        let mut cmd = SpecCommand::default();
391        cmd.name = "test".to_string();
392        cmd.args = vec![SpecArg {
393            name: "arg".to_string(),
394            ..Default::default()
395        }];
396        cmd.flags = vec![
397            SpecFlag {
398                name: "flag".to_string(),
399                long: vec!["flag".to_string()],
400                ..Default::default()
401            },
402            SpecFlag {
403                name: "force".to_string(),
404                long: vec!["force".to_string()],
405                negate: Some("--no-force".to_string()),
406                ..Default::default()
407            },
408        ];
409        let spec = Spec {
410            name: "test".to_string(),
411            bin: "test".to_string(),
412            cmd,
413            ..Default::default()
414        };
415        let input = vec![
416            "test".to_string(),
417            "--flag".to_string(),
418            "--no-force".to_string(),
419        ];
420        let parsed = parse(&spec, &input).unwrap();
421        let env = parsed.as_env();
422        assert_eq!(env.len(), 2);
423        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
424        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
425    }
426}