rust_args_parser/
parse.rs

1use crate::matches::{key_for, pos_key_for};
2#[cfg(feature = "suggest")]
3use crate::suggest::levenshtein;
4use crate::util::looks_like_number_token;
5use crate::{CmdSpec, Env, Error, GroupMode, Repeat, Result, Source};
6use crate::{Matches, Status, Value};
7use std::collections::HashMap;
8use std::ffi::{OsStr, OsString};
9
10/// Parse command line arguments.
11/// # Errors [`Error`]
12pub fn parse<'a, Ctx: ?Sized>(
13    env: &Env,
14    root: &'a CmdSpec<'a, Ctx>,
15    argv: &[OsString],
16    ctx: &mut Ctx,
17) -> Result<Matches> {
18    let mut m = Matches::new();
19    let mut cursor = ParseCursor::new(root);
20    cursor.eager_overlay_here(&mut m);
21    let mut i = 0usize;
22    while i < argv.len() {
23        let tok = &argv[i];
24        if !cursor.positional_only {
25            if tok == "--" {
26                i += 1;
27                cursor.positional_only = true;
28                continue;
29            }
30            if let Some(e) = try_handle_builtins(env, &cursor.stack, cursor.current, tok) {
31                return Err(e);
32            }
33            if let Some(sub) = try_select_subcommand(cursor.current, tok) {
34                cursor.descend(sub);
35                i += 1;
36                cursor.eager_overlay_here(&mut m);
37                continue;
38            }
39            if let Some(consumed) =
40                try_parse_long(env, cursor.current, &mut m, &cursor.path, &cursor.long_ix, argv, i)?
41            {
42                i += consumed;
43                continue;
44            }
45            if let Some(consumed) = try_parse_short_or_numeric(
46                env,
47                cursor.current,
48                &mut m,
49                &cursor.path,
50                &cursor.short_ix,
51                argv,
52                i,
53            )? {
54                i += consumed;
55                continue;
56            }
57            if let Some(s) = tok.to_str() {
58                if !s.starts_with('-')
59                    && !cursor.current.get_subcommands().is_empty()
60                    && cursor.current.get_positionals().get(cursor.pos_idx).is_none()
61                {
62                    return Err(unknown_command_error(env, s, cursor.current));
63                }
64            }
65        }
66        // Positional
67        if let Some(consumed) = try_push_positional(
68            cursor.current,
69            &mut m,
70            &cursor.path,
71            &mut cursor.pos_idx,
72            &mut cursor.pos_counts,
73            tok,
74        ) {
75            i += consumed;
76            continue;
77        }
78        return Err(Error::UnexpectedPositional { token: os_dbg(tok) });
79    }
80
81    walk_levels(&cursor.stack, |path, cmd| {
82        overlay_env_and_defaults(&mut m, path, cmd);
83        validate_level(&m, path, cmd)
84    })?;
85    walk_levels(&cursor.stack, |path, cmd| run_callbacks(&m, path, cmd, ctx))?;
86    // Execute **leaf** command handler if any
87    if let Some(leaf) = cursor.stack.last() {
88        if let Some(h) = leaf.get_handler() {
89            h(&m, ctx)?;
90        }
91    }
92    m.set_leaf_path(&cursor.path);
93    Ok(m)
94}
95
96// Unknown subcommand (with suggestions/aliases)
97#[cfg(feature = "suggest")]
98fn unknown_command_error<Ctx: ?Sized>(env: &Env, name: &str, cmd: &CmdSpec<'_, Ctx>) -> Error {
99    let suggestions = if env.suggest {
100        let mut cands: Vec<String> = Vec::new();
101        for sc in cmd.get_subcommands() {
102            cands.push(sc.get_name().to_string());
103            for a in sc.get_aliases() {
104                cands.push((*a).to_string());
105            }
106        }
107        cands.sort();
108        cands.dedup();
109        best_suggestions(name, &cands)
110    } else {
111        vec![]
112    };
113    Error::UnknownCommand { token: name.to_string(), suggestions }
114}
115#[cfg(not(feature = "suggest"))]
116fn unknown_command_error<Ctx: ?Sized>(_: &Env, name: &str, _: &CmdSpec<'_, Ctx>) -> Error {
117    Error::UnknownCommand { token: name.to_string(), suggestions: vec![] }
118}
119
120fn try_handle_builtins<Ctx: ?Sized>(
121    env: &Env,
122    stack: &[&CmdSpec<'_, Ctx>],
123    current: &CmdSpec<'_, Ctx>,
124    tok: &OsString,
125) -> Option<Error> {
126    let s = tok.to_str()?;
127    if env.auto_help && (s == "-h" || s == "--help") {
128        #[cfg(feature = "help")]
129        {
130            let names: Vec<&str> = stack.iter().map(|c| c.get_name()).collect();
131            let msg = crate::help::render_help_with_path(env, &names, current);
132            return Some(Error::ExitMsg { code: 0, message: Some(msg) });
133        }
134        #[cfg(not(feature = "help"))]
135        {
136            let _ = current;
137            return Some(Error::ExitMsg { code: 0, message: None });
138        }
139    }
140    if stack.len() == 1 {
141        if let Some(ver) = env.version {
142            if s == "-V" || s == "--version" {
143                return Some(Error::ExitMsg { code: 0, message: Some(ver.to_string()) });
144            }
145        }
146        if let Some(auth) = env.author {
147            if s == "-A" || s == "--author" {
148                return Some(Error::ExitMsg { code: 0, message: Some(auth.to_string()) });
149            }
150        }
151    }
152    None
153}
154
155fn try_select_subcommand<'a, Ctx: ?Sized>(
156    current: &'a CmdSpec<'a, Ctx>,
157    tok: &OsString,
158) -> Option<&'a CmdSpec<'a, Ctx>> {
159    let s = tok.to_str()?;
160    current.find_sub(s)
161}
162
163fn try_parse_long<'a, Ctx: ?Sized>(
164    env: &Env,
165    current: &CmdSpec<'a, Ctx>,
166    m: &mut Matches,
167    path: &[&str],
168    long_ix: &HashMap<&'a str, usize>,
169    argv: &[OsString],
170    i: usize,
171) -> Result<Option<usize>> {
172    let Some(s) = argv[i].to_str() else { return Ok(None) };
173    if !s.starts_with("--") {
174        return Ok(None);
175    }
176    let body = &s[2..];
177    let mut it = body.splitn(2, '=');
178    let Some(name) = it.next() else {
179        return Ok(None);
180    };
181    let val_inline = it.next();
182
183    let Some(&idx) = long_ix.get(name) else {
184        return Err(unknown_long_error(env, name, current, path));
185    };
186    let opt = &current.get_opts()[idx];
187    let key = key_for(path, opt.get_name());
188
189    if opt.is_value() {
190        let v = if let Some(v) = val_inline {
191            OsString::from(v)
192        } else {
193            argv.get(i + 1).cloned().ok_or(Error::MissingValue { opt: format!("--{name}") })?
194        };
195        set_val(m, &key, v, Source::Cli, opt.get_repeat());
196        Ok(Some(if val_inline.is_some() { 1 } else { 2 }))
197    } else {
198        set_flag(m, &key, Source::Cli);
199        Ok(Some(1))
200    }
201}
202
203fn try_parse_short_or_numeric<Ctx: ?Sized>(
204    env: &Env,
205    current: &CmdSpec<'_, Ctx>,
206    m: &mut Matches,
207    path: &[&str],
208    short_ix: &HashMap<char, usize>,
209    argv: &[OsString],
210    i: usize,
211) -> Result<Option<usize>> {
212    let Some(s) = argv[i].to_str() else { return Ok(None) };
213    let Some(rest) = s.strip_prefix('-') else { return Ok(None) };
214    if rest.is_empty() {
215        return Ok(None);
216    }
217
218    // Numeric fallback: if first char is not a known short and token looks numeric, treat as positional/value.
219    if let Some(first) = rest.chars().next() {
220        if short_ix.get(&first).is_none() && looks_like_number_token(s) {
221            return Ok(None);
222        }
223    }
224
225    // Cluster walk
226    let mut chars = rest.chars().peekable();
227    while let Some(c) = chars.next() {
228        let Some(&idx) = short_ix.get(&c) else {
229            return Err(unknown_short_error(env, c, current, path));
230        };
231        let opt = &current.get_opts()[idx];
232        let key = key_for(path, opt.get_name());
233        if opt.is_value() {
234            if chars.peek().is_some() {
235                let r: String = chars.collect();
236                set_val(m, &key, OsString::from(r), Source::Cli, opt.get_repeat());
237                return Ok(Some(1));
238            }
239            let v = argv.get(i + 1).cloned().ok_or(Error::MissingValue { opt: format!("-{c}") })?;
240            set_val(m, &key, v, Source::Cli, opt.get_repeat());
241            return Ok(Some(2));
242        }
243        set_flag(m, &key, Source::Cli);
244    }
245    Ok(Some(1))
246}
247
248fn try_push_positional<Ctx: ?Sized>(
249    current: &CmdSpec<'_, Ctx>,
250    m: &mut Matches,
251    path: &[&str],
252    pos_idx: &mut usize,
253    pos_counts: &mut [usize],
254    tok: &OsString,
255) -> Option<usize> {
256    let pos = current.get_positionals().get(*pos_idx)?;
257    let key = pos_key_for(path, pos.get_name());
258    push_pos(m, &key, tok.clone());
259    pos_counts[*pos_idx] += 1;
260    // advance if capacity reached
261    match pos.get_cardinality() {
262        crate::spec::PosCardinality::One { .. } => {
263            *pos_idx += 1;
264        }
265        crate::spec::PosCardinality::Many => { /* stay */ }
266        crate::spec::PosCardinality::Range { min: _, max } => {
267            if pos_counts[*pos_idx] >= max {
268                *pos_idx += 1;
269            }
270        }
271    }
272    Some(1)
273}
274
275fn rebuild_indexes<'a, Ctx: ?Sized>(
276    cmd: &'a CmdSpec<'_, Ctx>,
277    long: &mut HashMap<&'a str, usize>,
278    short: &mut HashMap<char, usize>,
279) {
280    long.clear();
281    short.clear();
282    for (i, o) in cmd.get_opts().iter().enumerate() {
283        if let Some(l) = o.get_long() {
284            long.insert(l, i);
285        }
286        if let Some(s) = o.get_short() {
287            short.insert(s, i);
288        }
289    }
290}
291
292fn eager_overlay<Ctx: ?Sized>(m: &mut Matches, path: &[&str], cmd: &CmdSpec<'_, Ctx>, src: Source) {
293    for o in cmd.get_opts() {
294        let k = key_for(path, o.get_name());
295        if !m.status.contains_key(&k) {
296            match src {
297                Source::Env => {
298                    if let Some(var) = o.get_env() {
299                        if let Some(v) = std::env::var_os(var) {
300                            if o.is_value() {
301                                set_val(m, &k, v, Source::Env, o.get_repeat());
302                            } else {
303                                set_flag(m, &k, Source::Env);
304                            }
305                        }
306                    }
307                }
308                Source::Default => {
309                    if let Some(d) = o.get_default() {
310                        set_val(m, &k, d.clone(), Source::Default, o.get_repeat());
311                    }
312                }
313                Source::Cli => {}
314            }
315        }
316    }
317}
318
319fn set_flag(m: &mut Matches, key: &str, src: Source) {
320    *m.flag_counts.entry(key.to_string()).or_insert(0) += 1;
321    m.values.insert(key.to_string(), Value::Flag);
322    m.status.insert(key.to_string(), Status::Set(src));
323}
324
325fn set_val(m: &mut Matches, key: &str, val: OsString, src: Source, rep: Repeat) {
326    match rep {
327        Repeat::Single => {
328            m.values.insert(key.to_string(), Value::One(val));
329        }
330        Repeat::Many => {
331            m.values
332                .entry(key.to_string())
333                .and_modify(|v| {
334                    if let Value::Many(vs) = v {
335                        vs.push(val.clone());
336                    }
337                })
338                .or_insert_with(|| Value::Many(vec![val]));
339        }
340    }
341    m.status.insert(key.to_string(), Status::Set(src));
342}
343fn push_pos(m: &mut Matches, key: &str, val: OsString) {
344    use crate::Value::{Flag, Many, One};
345    match m.values.get_mut(key) {
346        Some(Many(vs)) => vs.push(val),
347        Some(One(_) | Flag) => {
348            if let Some(One(s)) = m.values.remove(key) {
349                m.values.insert(key.to_string(), Many(vec![s, val]));
350            }
351        }
352        None => {
353            m.values.insert(key.to_string(), One(val));
354        }
355    }
356    m.status.insert(key.to_string(), Status::Set(Source::Cli));
357}
358
359fn os_dbg(s: &OsStr) -> String {
360    s.to_string_lossy().into_owned()
361}
362
363#[cfg(feature = "suggest")]
364fn unknown_long_error<Ctx: ?Sized>(
365    env: &Env,
366    name: &str,
367    cmd: &CmdSpec<'_, Ctx>,
368    path: &[&str],
369) -> Error {
370    let suggestions = if env.suggest {
371        let mut cands: Vec<String> = cmd
372            .get_opts()
373            .iter()
374            .filter_map(|o| o.get_long().map(std::string::ToString::to_string))
375            .collect();
376        if path.is_empty() {
377            if env.author.is_some() {
378                cands.push("author".to_string());
379            }
380            if env.version.is_some() {
381                cands.push("version".to_string());
382            }
383        }
384        cands.push("help".to_string());
385        cands.sort();
386        best_suggestions(name, &cands).into_iter().map(|s| format!("--{s}")).collect()
387    } else {
388        vec![]
389    };
390    Error::UnknownOption { token: format!("--{name}"), suggestions }
391}
392#[cfg(not(feature = "suggest"))]
393fn unknown_long_error<Ctx: ?Sized>(_: &Env, name: &str, _: &CmdSpec<'_, Ctx>, _: &[&str]) -> Error {
394    Error::UnknownOption { token: format!("--{}", name), suggestions: vec![] }
395}
396
397#[cfg(feature = "suggest")]
398fn unknown_short_error<Ctx: ?Sized>(
399    env: &Env,
400    c: char,
401    cmd: &CmdSpec<'_, Ctx>,
402    path: &[&str],
403) -> Error {
404    let suggestions = if env.suggest {
405        let mut cands: Vec<String> =
406            cmd.get_opts().iter().filter_map(|o| o.get_short().map(|s| s.to_string())).collect();
407        if path.is_empty() {
408            if env.author.is_some() {
409                cands.push("A".into());
410            }
411            if env.version.is_some() {
412                cands.push("V".into());
413            }
414        }
415        cands.push("h".into());
416        cands.sort();
417        best_suggestions(&c.to_string(), &cands).into_iter().map(|s| format!("-{s}")).collect()
418    } else {
419        vec![]
420    };
421    Error::UnknownOption { token: format!("-{c}"), suggestions }
422}
423#[cfg(not(feature = "suggest"))]
424fn unknown_short_error<Ctx: ?Sized>(_: &Env, c: char, _: &CmdSpec<'_, Ctx>, _: &[&str]) -> Error {
425    Error::UnknownOption { token: format!("-{}", c), suggestions: vec![] }
426}
427
428#[cfg(feature = "suggest")]
429fn best_suggestions(needle: &str, hay: &[String]) -> Vec<String> {
430    let mut scored: Vec<(usize, String)> =
431        hay.iter().map(|h| (levenshtein(needle, h), h.clone())).collect();
432    scored.sort_by_key(|(d, _)| *d);
433    scored.into_iter().filter(|(d, _)| *d <= 2).take(3).map(|(_, s)| s).collect()
434}
435
436/// Walk stack from root→leaf, yielding the *scoped* path (without root) and the cmd.
437fn walk_levels<'a, Ctx, F>(stack: &[&'a CmdSpec<'a, Ctx>], mut f: F) -> Result<()>
438where
439    Ctx: ?Sized,
440    F: FnMut(&[&'a str], &'a CmdSpec<'a, Ctx>) -> Result<()>,
441{
442    let mut path: Vec<&'a str> = Vec::with_capacity(stack.len().saturating_sub(1));
443    for (idx, cmd) in stack.iter().enumerate() {
444        if idx > 0 {
445            path.push(cmd.get_name());
446        }
447        f(&path, cmd)?;
448    }
449    Ok(())
450}
451
452fn overlay_env_and_defaults<Ctx: ?Sized>(m: &mut Matches, path: &[&str], cmd: &CmdSpec<'_, Ctx>) {
453    eager_overlay(m, path, cmd, crate::Source::Env);
454    eager_overlay(m, path, cmd, crate::Source::Default);
455}
456
457fn validate_level<'a, Ctx: ?Sized>(
458    m: &Matches,
459    path: &[&'a str],
460    cmd: &CmdSpec<'a, Ctx>,
461) -> Result<()> {
462    use crate::spec::PosCardinality;
463    use crate::Value;
464
465    // Positionals: required + Range{min} check
466    for p in cmd.get_positionals() {
467        let k = pos_key_for(path, p.get_name());
468        if p.get_cardinality() == (PosCardinality::One { required: true })
469            && !m.values.contains_key(&k)
470        {
471            return Err(Error::User("missing required positional".into()));
472        }
473        if let PosCardinality::Range { min, .. } = p.get_cardinality() {
474            let count = match m.values.get(&k) {
475                Some(Value::One(_)) => 1,
476                Some(Value::Many(vs)) => vs.len(),
477                _ => 0,
478            };
479            if count < min {
480                return Err(Error::User("positional count below minimum".into()));
481            }
482        }
483    }
484
485    // Groups: Xor/ReqOne like in your code
486    for g in cmd.get_groups() {
487        let mut hits = 0u32;
488        for o in cmd.get_opts() {
489            if o.get_group() == Some(g.name) && m.status.contains_key(&key_for(path, o.get_name()))
490            {
491                hits += 1;
492            }
493        }
494        match g.mode {
495            GroupMode::Xor if hits > 1 => {
496                return Err(Error::User(format!(
497                    "options in group '{}' are mutually exclusive",
498                    g.name
499                )))
500            }
501            GroupMode::ReqOne if hits == 0 => {
502                return Err(Error::User(format!(
503                    "one of the options in group '{}' is required",
504                    g.name
505                )))
506            }
507            _ => {}
508        }
509    }
510
511    // Option validators
512    for o in cmd.get_opts() {
513        if let Some(vf) = o.get_validator() {
514            match m.values.get(&key_for(path, o.get_name())) {
515                Some(Value::One(v)) => vf(v.as_os_str())?,
516                Some(Value::Many(vs)) => {
517                    for v in vs {
518                        vf(v.as_os_str())?;
519                    }
520                }
521                _ => {}
522            }
523        }
524    }
525
526    // Positional validators
527    for p in cmd.get_positionals() {
528        if let Some(vf) = p.get_validator() {
529            match m.values.get(&pos_key_for(path, p.get_name())) {
530                Some(Value::One(v)) => vf(v.as_os_str())?,
531                Some(Value::Many(vs)) => {
532                    for v in vs {
533                        vf(v.as_os_str())?;
534                    }
535                }
536                _ => {}
537            }
538        }
539    }
540
541    // Command-level validator
542    if let Some(cv) = cmd.get_validator() {
543        cv(m)?;
544    }
545
546    Ok(())
547}
548
549fn run_callbacks<'a, Ctx: ?Sized>(
550    m: &Matches,
551    path: &[&'a str],
552    cmd: &CmdSpec<'a, Ctx>,
553    ctx: &mut Ctx,
554) -> Result<()> {
555    use crate::Value;
556
557    // options
558    for o in cmd.get_opts() {
559        let k = key_for(path, o.get_name());
560        match m.values.get(&k) {
561            Some(Value::Flag) => {
562                if let Some(cb) = o.get_on_flag() {
563                    let n = *m.flag_counts.get(&k).unwrap_or(&1);
564                    for _ in 0..n {
565                        cb(ctx)?;
566                    }
567                }
568            }
569            Some(Value::One(v)) => {
570                if let Some(cb) = o.get_on_value() {
571                    cb(v.as_os_str(), ctx)?;
572                }
573            }
574            Some(Value::Many(vs)) => {
575                if let Some(cb) = o.get_on_value() {
576                    for v in vs {
577                        cb(v.as_os_str(), ctx)?;
578                    }
579                }
580            }
581            None => {}
582        }
583    }
584
585    // positionals
586    for p in cmd.get_positionals() {
587        let k = pos_key_for(path, p.get_name());
588        match m.values.get(&k) {
589            Some(Value::One(v)) => (p.get_on_value())(v.as_os_str(), ctx)?,
590            Some(Value::Many(vs)) => {
591                for v in vs {
592                    (p.get_on_value())(v.as_os_str(), ctx)?;
593                }
594            }
595            _ => {}
596        }
597    }
598
599    Ok(())
600}
601
602struct ParseCursor<'a, Ctx: ?Sized> {
603    path: Vec<&'a str>,
604    stack: Vec<&'a CmdSpec<'a, Ctx>>,
605    current: &'a CmdSpec<'a, Ctx>,
606    long_ix: HashMap<&'a str, usize>,
607    short_ix: HashMap<char, usize>,
608    positional_only: bool,
609    pos_idx: usize,
610    pos_counts: Vec<usize>,
611}
612
613impl<'a, Ctx: ?Sized> ParseCursor<'a, Ctx> {
614    fn new(root: &'a CmdSpec<'a, Ctx>) -> Self {
615        let mut cur = Self {
616            path: Vec::new(),
617            stack: vec![root],
618            current: root,
619            long_ix: HashMap::new(),
620            short_ix: HashMap::new(),
621            positional_only: false,
622            pos_idx: 0,
623            pos_counts: vec![0; root.get_positionals().len()],
624        };
625        rebuild_indexes(cur.current, &mut cur.long_ix, &mut cur.short_ix);
626        cur
627    }
628    fn rebuild_indexes(&mut self) {
629        rebuild_indexes(self.current, &mut self.long_ix, &mut self.short_ix);
630    }
631    fn descend(&mut self, sub: &'a CmdSpec<'a, Ctx>) {
632        self.stack.push(sub);
633        self.path.push(sub.get_name());
634        self.current = sub;
635        self.positional_only = false;
636        self.pos_idx = 0;
637        self.pos_counts = vec![0; self.current.get_positionals().len()];
638        self.rebuild_indexes();
639    }
640    fn eager_overlay_here(&self, m: &mut Matches) {
641        eager_overlay(m, &self.path, self.current, Source::Env);
642        eager_overlay(m, &self.path, self.current, Source::Default);
643    }
644}