Skip to main content

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