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
10pub 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 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 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#[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 = ¤t.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 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 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 = ¤t.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 match pos.get_cardinality() {
262 crate::spec::PosCardinality::One { .. } => {
263 *pos_idx += 1;
264 }
265 crate::spec::PosCardinality::Many => { }
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
436fn 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 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 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 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 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 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 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 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}