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
37    for arg in out.cmd.args.iter().skip(out.args.len()) {
39        if let Some(env_var) = arg.env.as_ref() {
40            if let Ok(env_value) = std::env::var(env_var) {
41                out.args.insert(arg.clone(), ParseValue::String(env_value));
42                continue;
43            }
44        }
45        if let Some(default) = arg.default.as_ref() {
46            out.args
47                .insert(arg.clone(), ParseValue::String(default.clone()));
48        }
49    }
50
51    for flag in out.available_flags.values() {
53        if out.flags.contains_key(flag) {
54            continue;
55        }
56        if let Some(env_var) = flag.env.as_ref() {
57            if let Ok(env_value) = std::env::var(env_var) {
58                if flag.arg.is_some() {
59                    out.flags
60                        .insert(flag.clone(), ParseValue::String(env_value));
61                } else {
62                    let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
64                    out.flags.insert(flag.clone(), ParseValue::Bool(is_true));
65                }
66                continue;
67            }
68        }
69        if let Some(default) = flag.default.as_ref() {
70            out.flags
71                .insert(flag.clone(), ParseValue::String(default.clone()));
72        }
73        if let Some(Some(default)) = flag.arg.as_ref().map(|a| &a.default) {
74            out.flags
75                .insert(flag.clone(), ParseValue::String(default.clone()));
76        }
77    }
78    if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
79        bail!("{err}");
80    }
81    if !out.errors.is_empty() {
82        bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
83    }
84    Ok(out)
85}
86
87pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
88    trace!("parse_partial: {input:?}");
89    let mut input = input.iter().cloned().collect::<VecDeque<_>>();
90    input.pop_front();
91
92    let gather_flags = |cmd: &SpecCommand| {
93        cmd.flags
94            .iter()
95            .flat_map(|f| {
96                let mut flags = f
97                    .long
98                    .iter()
99                    .map(|l| (format!("--{l}"), f.clone()))
100                    .chain(f.short.iter().map(|s| (format!("-{s}"), f.clone())))
101                    .collect::<Vec<_>>();
102                if let Some(negate) = &f.negate {
103                    flags.push((negate.clone(), f.clone()));
104                }
105                flags
106            })
107            .collect()
108    };
109
110    let mut out = ParseOutput {
111        cmd: spec.cmd.clone(),
112        cmds: vec![spec.cmd.clone()],
113        args: IndexMap::new(),
114        flags: IndexMap::new(),
115        available_flags: gather_flags(&spec.cmd),
116        flag_awaiting_value: vec![],
117        errors: vec![],
118    };
119
120    while !input.is_empty() {
121        if let Some(subcommand) = out.cmd.find_subcommand(&input[0]) {
122            let mut subcommand = subcommand.clone();
123            subcommand.mount()?;
124            out.available_flags.retain(|_, f| f.global);
125            out.available_flags.extend(gather_flags(&subcommand));
126            input.pop_front();
127            out.cmds.push(subcommand.clone());
128            out.cmd = subcommand.clone();
129        } else {
130            break;
131        }
132    }
133
134    let mut next_arg = out.cmd.args.first();
135    let mut enable_flags = true;
136    let mut grouped_flag = false;
137
138    while !input.is_empty() {
139        let mut w = input.pop_front().unwrap();
140
141        if w == "--" {
142            enable_flags = false;
143            continue;
144        }
145
146        if enable_flags && w.starts_with("--") {
148            grouped_flag = false;
149            let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
150            if !val.is_empty() {
151                input.push_front(val.to_string());
152            }
153            if let Some(f) = out.available_flags.get(word) {
154                if f.arg.is_some() {
155                    out.flag_awaiting_value.push(f.clone());
156                } else if f.var {
157                    let arr = out
158                        .flags
159                        .entry(f.clone())
160                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
161                        .try_as_multi_bool_mut()
162                        .unwrap();
163                    arr.push(true);
164                } else {
165                    let negate = f.negate.clone().unwrap_or_default();
166                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
167                }
168                continue;
169            }
170            if is_help_arg(spec, &w) {
171                out.errors
172                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
173                return Ok(out);
174            }
175        }
176
177        if enable_flags && w.starts_with('-') && w.len() > 1 {
179            let short = w.chars().nth(1).unwrap();
180            if let Some(f) = out.available_flags.get(&format!("-{short}")) {
181                if w.len() > 2 {
182                    input.push_front(format!("-{}", &w[2..]));
183                    grouped_flag = true;
184                }
185                if f.arg.is_some() {
186                    out.flag_awaiting_value.push(f.clone());
187                } else if f.var {
188                    let arr = out
189                        .flags
190                        .entry(f.clone())
191                        .or_insert_with(|| ParseValue::MultiBool(vec![]))
192                        .try_as_multi_bool_mut()
193                        .unwrap();
194                    arr.push(true);
195                } else {
196                    let negate = f.negate.clone().unwrap_or_default();
197                    out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
198                }
199                continue;
200            }
201            if is_help_arg(spec, &w) {
202                out.errors
203                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
204                return Ok(out);
205            }
206            if grouped_flag {
207                grouped_flag = false;
208                w.remove(0);
209            }
210        }
211
212        if !out.flag_awaiting_value.is_empty() {
213            while let Some(flag) = out.flag_awaiting_value.pop() {
214                let arg = flag.arg.as_ref().unwrap();
215                if flag.var {
216                    let arr = out
217                        .flags
218                        .entry(flag)
219                        .or_insert_with(|| ParseValue::MultiString(vec![]))
220                        .try_as_multi_string_mut()
221                        .unwrap();
222                    arr.push(w);
223                } else {
224                    if let Some(choices) = &arg.choices {
225                        if !choices.choices.contains(&w) {
226                            if is_help_arg(spec, &w) {
227                                out.errors
228                                    .push(render_help_err(spec, &out.cmd, w.len() > 2));
229                                return Ok(out);
230                            }
231                            bail!(
232                                "Invalid choice for option {}: {w}, expected one of {}",
233                                flag.name,
234                                choices.choices.join(", ")
235                            );
236                        }
237                    }
238                    out.flags.insert(flag, ParseValue::String(w));
239                }
240                w = "".to_string();
241            }
242            continue;
243        }
244
245        if let Some(arg) = next_arg {
246            if arg.var {
247                let arr = out
248                    .args
249                    .entry(arg.clone())
250                    .or_insert_with(|| ParseValue::MultiString(vec![]))
251                    .try_as_multi_string_mut()
252                    .unwrap();
253                arr.push(w);
254                if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
255                    next_arg = out.cmd.args.get(out.args.len());
256                }
257            } else {
258                if let Some(choices) = &arg.choices {
259                    if !choices.choices.contains(&w) {
260                        if is_help_arg(spec, &w) {
261                            out.errors
262                                .push(render_help_err(spec, &out.cmd, w.len() > 2));
263                            return Ok(out);
264                        }
265                        bail!(
266                            "Invalid choice for arg {}: {w}, expected one of {}",
267                            arg.name,
268                            choices.choices.join(", ")
269                        );
270                    }
271                }
272                out.args.insert(arg.clone(), ParseValue::String(w));
273                next_arg = out.cmd.args.get(out.args.len());
274            }
275            continue;
276        }
277        if is_help_arg(spec, &w) {
278            out.errors
279                .push(render_help_err(spec, &out.cmd, w.len() > 2));
280            return Ok(out);
281        }
282        bail!("unexpected word: {w}");
283    }
284
285    for arg in out.cmd.args.iter().skip(out.args.len()) {
286        if arg.required && arg.default.is_none() {
287            let has_env = arg
289                .env
290                .as_ref()
291                .map(|e| std::env::var(e).is_ok())
292                .unwrap_or(false);
293            if !has_env {
294                out.errors.push(UsageErr::MissingArg(arg.name.clone()));
295            }
296        }
297    }
298
299    for flag in out.available_flags.values() {
300        if out.flags.contains_key(flag) {
301            continue;
302        }
303        let has_default = flag.default.is_some() || flag.arg.iter().any(|a| a.default.is_some());
304        let has_env = flag
305            .env
306            .as_ref()
307            .map(|e| std::env::var(e).is_ok())
308            .unwrap_or(false);
309        if flag.required && !has_default && !has_env {
310            out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
311        }
312    }
313
314    Ok(out)
315}
316
317#[cfg(feature = "docs")]
318fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
319    UsageErr::Help(docs::cli::render_help(spec, cmd, long))
320}
321
322#[cfg(not(feature = "docs"))]
323fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
324    UsageErr::Help("help".to_string())
325}
326
327fn is_help_arg(spec: &Spec, w: &str) -> bool {
328    spec.disable_help != Some(true)
329        && (w == "--help"
330            || w == "-h"
331            || w == "-?"
332            || (spec.cmd.subcommands.is_empty() && w == "help"))
333}
334
335impl ParseOutput {
336    pub fn as_env(&self) -> BTreeMap<String, String> {
337        let mut env = BTreeMap::new();
338        for (flag, val) in &self.flags {
339            let key = format!("usage_{}", flag.name.to_snake_case());
340            let val = match val {
341                ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
342                ParseValue::String(s) => s.clone(),
343                ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
344                ParseValue::MultiString(s) => shell_words::join(s),
345            };
346            env.insert(key, val);
347        }
348        for (arg, val) in &self.args {
349            let key = format!("usage_{}", arg.name.to_snake_case());
350            env.insert(key, val.to_string());
351        }
352        env
353    }
354}
355
356impl Display for ParseValue {
357    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358        match self {
359            ParseValue::Bool(b) => write!(f, "{b}"),
360            ParseValue::String(s) => write!(f, "{s}"),
361            ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
362            ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
363        }
364    }
365}
366
367impl Debug for ParseOutput {
368    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
369        f.debug_struct("ParseOutput")
370            .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
371            .field(
372                "args",
373                &self
374                    .args
375                    .iter()
376                    .map(|(a, w)| format!("{}: {w}", &a.name))
377                    .collect_vec(),
378            )
379            .field(
380                "available_flags",
381                &self
382                    .available_flags
383                    .iter()
384                    .map(|(f, w)| format!("{f}: {w}"))
385                    .collect_vec(),
386            )
387            .field(
388                "flags",
389                &self
390                    .flags
391                    .iter()
392                    .map(|(f, w)| format!("{}: {w}", &f.name))
393                    .collect_vec(),
394            )
395            .field("flag_awaiting_value", &self.flag_awaiting_value)
396            .field("errors", &self.errors)
397            .finish()
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn test_parse() {
407        let mut cmd = SpecCommand::default();
408        cmd.name = "test".to_string();
409        cmd.args = vec![SpecArg {
410            name: "arg".to_string(),
411            ..Default::default()
412        }];
413        cmd.flags = vec![SpecFlag {
414            name: "flag".to_string(),
415            long: vec!["flag".to_string()],
416            ..Default::default()
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!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
425        let parsed = parse(&spec, &input).unwrap();
426        assert_eq!(parsed.cmds.len(), 1);
427        assert_eq!(parsed.cmds[0].name, "test");
428        assert_eq!(parsed.args.len(), 1);
429        assert_eq!(parsed.flags.len(), 1);
430        assert_eq!(parsed.available_flags.len(), 1);
431    }
432
433    #[test]
434    fn test_as_env() {
435        let mut cmd = SpecCommand::default();
436        cmd.name = "test".to_string();
437        cmd.args = vec![SpecArg {
438            name: "arg".to_string(),
439            ..Default::default()
440        }];
441        cmd.flags = vec![
442            SpecFlag {
443                name: "flag".to_string(),
444                long: vec!["flag".to_string()],
445                ..Default::default()
446            },
447            SpecFlag {
448                name: "force".to_string(),
449                long: vec!["force".to_string()],
450                negate: Some("--no-force".to_string()),
451                ..Default::default()
452            },
453        ];
454        let spec = Spec {
455            name: "test".to_string(),
456            bin: "test".to_string(),
457            cmd,
458            ..Default::default()
459        };
460        let input = vec![
461            "test".to_string(),
462            "--flag".to_string(),
463            "--no-force".to_string(),
464        ];
465        let parsed = parse(&spec, &input).unwrap();
466        let env = parsed.as_env();
467        assert_eq!(env.len(), 2);
468        assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
469        assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
470    }
471
472    #[test]
473    fn test_arg_env_var() {
474        let mut cmd = SpecCommand::default();
475        cmd.name = "test".to_string();
476        cmd.args = vec![SpecArg {
477            name: "input".to_string(),
478            env: Some("TEST_ARG_INPUT".to_string()),
479            required: true,
480            ..Default::default()
481        }];
482        let spec = Spec {
483            name: "test".to_string(),
484            bin: "test".to_string(),
485            cmd,
486            ..Default::default()
487        };
488
489        std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
491
492        let input = vec!["test".to_string()];
493        let parsed = parse(&spec, &input).unwrap();
494
495        assert_eq!(parsed.args.len(), 1);
496        let arg = parsed.args.keys().next().unwrap();
497        assert_eq!(arg.name, "input");
498        let value = parsed.args.values().next().unwrap();
499        assert_eq!(value.to_string(), "test_file.txt");
500
501        std::env::remove_var("TEST_ARG_INPUT");
503    }
504
505    #[test]
506    fn test_flag_env_var_with_arg() {
507        let mut cmd = SpecCommand::default();
508        cmd.name = "test".to_string();
509        cmd.flags = vec![SpecFlag {
510            name: "output".to_string(),
511            long: vec!["output".to_string()],
512            env: Some("TEST_FLAG_OUTPUT".to_string()),
513            arg: Some(SpecArg {
514                name: "file".to_string(),
515                ..Default::default()
516            }),
517            ..Default::default()
518        }];
519        let spec = Spec {
520            name: "test".to_string(),
521            bin: "test".to_string(),
522            cmd,
523            ..Default::default()
524        };
525
526        std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
528
529        let input = vec!["test".to_string()];
530        let parsed = parse(&spec, &input).unwrap();
531
532        assert_eq!(parsed.flags.len(), 1);
533        let flag = parsed.flags.keys().next().unwrap();
534        assert_eq!(flag.name, "output");
535        let value = parsed.flags.values().next().unwrap();
536        assert_eq!(value.to_string(), "output.txt");
537
538        std::env::remove_var("TEST_FLAG_OUTPUT");
540    }
541
542    #[test]
543    fn test_flag_env_var_boolean() {
544        let mut cmd = SpecCommand::default();
545        cmd.name = "test".to_string();
546        cmd.flags = vec![SpecFlag {
547            name: "verbose".to_string(),
548            long: vec!["verbose".to_string()],
549            env: Some("TEST_FLAG_VERBOSE".to_string()),
550            ..Default::default()
551        }];
552        let spec = Spec {
553            name: "test".to_string(),
554            bin: "test".to_string(),
555            cmd,
556            ..Default::default()
557        };
558
559        std::env::set_var("TEST_FLAG_VERBOSE", "true");
561
562        let input = vec!["test".to_string()];
563        let parsed = parse(&spec, &input).unwrap();
564
565        assert_eq!(parsed.flags.len(), 1);
566        let flag = parsed.flags.keys().next().unwrap();
567        assert_eq!(flag.name, "verbose");
568        let value = parsed.flags.values().next().unwrap();
569        assert_eq!(value.to_string(), "true");
570
571        std::env::remove_var("TEST_FLAG_VERBOSE");
573    }
574
575    #[test]
576    fn test_env_var_precedence() {
577        let mut cmd = SpecCommand::default();
579        cmd.name = "test".to_string();
580        cmd.args = vec![SpecArg {
581            name: "input".to_string(),
582            env: Some("TEST_PRECEDENCE_INPUT".to_string()),
583            required: true,
584            ..Default::default()
585        }];
586        let spec = Spec {
587            name: "test".to_string(),
588            bin: "test".to_string(),
589            cmd,
590            ..Default::default()
591        };
592
593        std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
595
596        let input = vec!["test".to_string(), "cli_file.txt".to_string()];
597        let parsed = parse(&spec, &input).unwrap();
598
599        assert_eq!(parsed.args.len(), 1);
600        let value = parsed.args.values().next().unwrap();
601        assert_eq!(value.to_string(), "cli_file.txt");
603
604        std::env::remove_var("TEST_PRECEDENCE_INPUT");
606    }
607}