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) = 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 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 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#[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 = ¤t.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 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 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 = ¤t.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 match pos.get_cardinality() {
250 crate::spec::PosCardinality::One { .. } => {
251 *pos_idx += 1;
252 }
253 crate::spec::PosCardinality::Many => { }
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
410fn 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 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 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 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 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 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 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 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}