1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3
4use std::env;
9use std::fmt::{self, Write as _};
10use std::io::{self, Write};
11
12type BoxError = Box<dyn std::error::Error>;
14pub type Result<T> = std::result::Result<T, Error>;
16
17pub type OptCallback<Ctx> =
19 for<'a> fn(Option<&'a str>, &mut Ctx) -> std::result::Result<(), BoxError>;
20
21pub type RunCallback<Ctx> = fn(&[&str], &mut Ctx) -> std::result::Result<(), BoxError>;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ArgKind {
27 None,
29 Required,
31 Optional,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum GroupMode {
38 None,
40 Xor,
42 ReqOne,
44}
45#[derive(Clone, Copy)]
47pub enum ValueHint {
48 Any,
50 Number,
52}
53#[derive(Clone, Copy)]
55pub struct OptSpec<'a, Ctx: ?Sized> {
56 name: &'a str, short: Option<char>, arg: ArgKind, metavar: Option<&'a str>, help: &'a str, env: Option<&'a str>, default: Option<&'a str>, group_id: u16, group_mode: GroupMode, value_hint: ValueHint, cb: OptCallback<Ctx>, }
68
69impl<'a, Ctx: ?Sized> OptSpec<'a, Ctx> {
70 pub const fn new(name: &'a str, cb: OptCallback<Ctx>) -> Self {
72 Self {
73 name,
74 short: None,
75 arg: ArgKind::None,
76 metavar: None,
77 help: "",
78 env: None,
79 default: None,
80 group_id: 0,
81 group_mode: GroupMode::None,
82 value_hint: ValueHint::Any,
83 cb,
84 }
85 }
86 #[must_use]
88 pub const fn numeric(mut self) -> Self {
89 self.value_hint = ValueHint::Number;
90 self
91 }
92 #[must_use]
94 pub const fn short(mut self, short: char) -> Self {
95 self.short = Some(short);
96 self
97 }
98 #[must_use]
100 pub const fn metavar(mut self, metavar: &'a str) -> Self {
101 self.metavar = Some(metavar);
102 self
103 }
104 #[must_use]
106 pub const fn help(mut self, help: &'a str) -> Self {
107 self.help = help;
108 self
109 }
110 #[must_use]
112 pub const fn arg(mut self, arg: ArgKind) -> Self {
113 self.arg = arg;
114 self
115 }
116 #[must_use]
118 pub const fn optional(mut self) -> Self {
119 self.arg = ArgKind::Optional;
120 self
121 }
122 #[must_use]
124 pub const fn required(mut self) -> Self {
125 self.arg = ArgKind::Required;
126 self
127 }
128 #[must_use]
130 pub const fn flag(mut self) -> Self {
131 self.arg = ArgKind::None;
132 self
133 }
134 #[must_use]
136 pub const fn env(mut self, env: &'a str) -> Self {
137 self.env = Some(env);
138 self
139 }
140 #[must_use]
142 pub const fn default(mut self, val: &'a str) -> Self {
143 self.default = Some(val);
144 self
145 }
146 #[must_use]
148 pub const fn at_most_one(mut self, group_id: u16) -> Self {
149 self.group_id = group_id;
150 self.group_mode = GroupMode::Xor;
151 self
152 }
153 #[must_use]
155 pub const fn at_least_one(mut self, group_id: u16) -> Self {
156 self.group_id = group_id;
157 self.group_mode = GroupMode::ReqOne;
158 self
159 }
160}
161
162#[derive(Clone, Copy)]
164pub struct PosSpec<'a> {
165 name: &'a str,
166 desc: Option<&'a str>,
167 min: usize,
168 max: usize,
169}
170
171impl<'a> PosSpec<'a> {
172 #[must_use]
174 pub const fn new(name: &'a str) -> Self {
175 Self { name, desc: None, min: 0, max: 0 }
176 }
177 #[must_use]
179 pub const fn desc(mut self, desc: &'a str) -> Self {
180 self.desc = Some(desc);
181 self
182 }
183 #[must_use]
185 pub const fn one(mut self) -> Self {
186 self.min = 1;
187 self.max = 1;
188 self
189 }
190 #[must_use]
192 pub const fn range(mut self, min: usize, max: usize) -> Self {
193 self.min = min;
194 self.max = max;
195 self
196 }
197}
198
199pub struct CmdSpec<'a, Ctx: ?Sized> {
201 name: Option<&'a str>, desc: Option<&'a str>,
203 opts: Box<[OptSpec<'a, Ctx>]>,
204 subs: Box<[CmdSpec<'a, Ctx>]>,
205 pos: Box<[PosSpec<'a>]>,
206 aliases: Box<[&'a str]>,
207 run: Option<RunCallback<Ctx>>, }
209
210impl<'a, Ctx: ?Sized> CmdSpec<'a, Ctx> {
211 #[must_use]
214 pub fn new(name: Option<&'a str>, run: Option<RunCallback<Ctx>>) -> Self {
215 Self {
216 name,
217 desc: None,
218 opts: Vec::new().into_boxed_slice(),
219 subs: Vec::new().into_boxed_slice(),
220 pos: Vec::new().into_boxed_slice(),
221 aliases: Vec::new().into_boxed_slice(),
222 run,
223 }
224 }
225 #[must_use]
227 pub const fn desc(mut self, desc: &'a str) -> Self {
228 self.desc = Some(desc);
229 self
230 }
231 #[must_use]
233 pub fn opts<S>(mut self, s: S) -> Self
234 where
235 S: Into<Vec<OptSpec<'a, Ctx>>>,
236 {
237 self.opts = s.into().into_boxed_slice();
238 self
239 }
240 #[must_use]
242 pub fn pos<S>(mut self, s: S) -> Self
243 where
244 S: Into<Vec<PosSpec<'a>>>,
245 {
246 self.pos = s.into().into_boxed_slice();
247 self
248 }
249 #[must_use]
251 pub fn subs<S>(mut self, s: S) -> Self
252 where
253 S: Into<Vec<Self>>,
254 {
255 self.subs = s.into().into_boxed_slice();
256 self
257 }
258 #[must_use]
260 pub fn aliases<S>(mut self, s: S) -> Self
261 where
262 S: Into<Vec<&'a str>>,
263 {
264 self.aliases = s.into().into_boxed_slice();
265 self
266 }
267}
268
269pub struct Env<'a> {
271 name: &'a str,
272 version: Option<&'a str>,
273 author: Option<&'a str>,
274 auto_help: bool,
275 wrap_cols: usize,
276 color: bool,
277}
278
279impl<'a> Env<'a> {
280 #[must_use]
282 pub const fn new(name: &'a str) -> Self {
283 Self { name, version: None, author: None, auto_help: false, wrap_cols: 0, color: false }
284 }
285 #[must_use]
287 pub const fn version(mut self, version: &'a str) -> Self {
288 self.version = Some(version);
289 self
290 }
291 #[must_use]
293 pub const fn author(mut self, author: &'a str) -> Self {
294 self.author = Some(author);
295 self
296 }
297 #[must_use]
299 pub const fn auto_help(mut self, auto_help: bool) -> Self {
300 self.auto_help = auto_help;
301 self
302 }
303 #[must_use]
305 pub const fn wrap_cols(mut self, wrap_cols: usize) -> Self {
306 self.wrap_cols = wrap_cols;
307 self
308 }
309 #[must_use]
311 pub const fn color(mut self, color: bool) -> Self {
312 self.color = color;
313 self
314 }
315 #[must_use]
318 pub fn auto_color(mut self) -> Self {
319 self.color = env::var("NO_COLOR").is_err();
320 self
321 }
322}
323
324pub fn dispatch_to<Ctx: ?Sized, W: Write>(
329 env: &Env<'_>,
330 root: &CmdSpec<'_, Ctx>,
331 argv: &[&str],
332 context: &mut Ctx,
333 out: &mut W,
334) -> Result<()> {
335 let mut idx = 0usize;
336 let mut cmd = root;
337 let mut chain: Vec<&str> = Vec::new();
338 while idx < argv.len() {
339 if let Some(next) = find_sub(cmd, argv[idx]) {
340 chain.push(argv[idx]);
341 cmd = next;
342 idx += 1;
343 } else {
344 break;
345 }
346 }
347 if !cmd.subs.is_empty() && cmd.pos.is_empty() && idx < argv.len() {
350 let tok = argv[idx];
351 if !tok.starts_with('-') && tok != "--" && find_sub(cmd, tok).is_none() {
352 return Err(unknown_command_error(cmd, tok));
353 }
354 }
355 let mut gcounts: Vec<u8> = vec![0; cmd.opts.len()];
357 let mut pos: Vec<&str> = Vec::with_capacity(argv.len().saturating_sub(idx));
359 let mut stop_opts = false;
360 while idx < argv.len() {
361 let tok = argv[idx];
362 if !stop_opts {
363 if tok == "--" {
364 stop_opts = true;
365 idx += 1;
366 continue;
367 }
368 if tok.starts_with("--") {
369 idx += 1;
370 parse_long(env, cmd, tok, &mut idx, argv, context, &mut gcounts, out, &chain)?;
371 continue;
372 }
373 if is_short_like(tok) {
374 idx += 1;
375 parse_short_cluster(
376 env,
377 cmd,
378 tok,
379 &mut idx,
380 argv,
381 context,
382 &mut gcounts,
383 out,
384 &chain,
385 )?;
386 continue;
387 }
388 if !tok.starts_with('-') && tok != "--" && cmd.pos.is_empty() {
389 if let Some(next) = find_sub(cmd, tok) {
390 apply_env_and_defaults(cmd, context, &mut gcounts)?;
391 check_groups(cmd, &gcounts)?;
392 chain.push(tok);
393 cmd = next;
394 idx += 1;
395 gcounts = vec![0; cmd.opts.len()];
396 pos.clear();
397 continue;
398 }
399 }
400 }
401 pos.push(tok);
402 idx += 1;
403 }
404 if cmd.pos.is_empty() && !pos.is_empty() {
406 return Err(Error::UnexpectedArgument(pos[0].to_string()));
407 }
408 apply_env_and_defaults(cmd, context, &mut gcounts)?;
409 check_groups(cmd, &gcounts)?;
411 validate_positionals(cmd, &pos)?;
413 if let Some(run) = cmd.run {
415 return run(&pos, context).map_err(Error::Callback);
416 }
417 if env.auto_help {
418 print_help_to(env, cmd, &chain, out);
419 }
420 Err(Error::Exit(1))
421}
422
423pub fn dispatch<Ctx>(
427 env: &Env<'_>,
428 root: &CmdSpec<'_, Ctx>,
429 argv: &[&str],
430 context: &mut Ctx,
431) -> Result<()> {
432 let mut out = io::stdout();
433 dispatch_to(env, root, argv, context, &mut out)
434}
435
436#[inline]
438fn format_alternates(items: &[String]) -> String {
439 match items.len() {
440 0 => String::new(),
441 1 => format!("'{}'", items[0]),
442 2 => format!("'{}' or '{}'", items[0], items[1]),
443 _ => {
444 let mut s = String::new();
446 for (i, it) in items.iter().enumerate() {
447 if i > 0 {
448 s.push_str(if i + 1 == items.len() { ", or " } else { ", " });
449 }
450 s.push('\'');
451 s.push_str(it);
452 s.push('\'');
453 }
454 s
455 }
456 }
457}
458
459#[inline]
460const fn max_distance_for(len: usize) -> usize {
461 match len {
462 0..=3 => 1,
463 4..=6 => 2,
464 _ => 3,
465 }
466}
467
468fn lev(a: &str, b: &str) -> usize {
470 let (na, nb) = (a.len(), b.len());
471 if na == 0 {
472 return nb;
473 }
474 if nb == 0 {
475 return na;
476 }
477 let mut prev: Vec<usize> = (0..=nb).collect();
478 let mut curr = vec![0; nb + 1];
479 for (i, ca) in a.chars().enumerate() {
480 curr[0] = i + 1;
481 for (j, cb) in b.chars().enumerate() {
482 let cost = usize::from(ca != cb);
483 curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
484 }
485 std::mem::swap(&mut prev, &mut curr);
486 }
487 prev[nb]
488}
489
490fn collect_long_candidates<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>) -> Vec<String> {
491 let mut v = Vec::with_capacity(cmd.opts.len() + 3);
492 if env.auto_help {
493 v.push("help".to_string());
494 }
495 if cmd.name.is_none() {
496 v.push("version".to_string());
497 v.push("author".to_string());
498 }
499 for o in &cmd.opts {
500 v.push(o.name.to_string());
501 }
502 v
503}
504
505fn collect_short_candidates<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>) -> Vec<char> {
506 let mut v = Vec::with_capacity(cmd.opts.len() + 3);
507 if env.auto_help {
508 v.push('h');
509 }
510 if cmd.name.is_none() {
511 v.push('V');
512 v.push('A');
513 }
514 for o in &cmd.opts {
515 if let Some(ch) = o.short {
516 v.push(ch);
517 }
518 }
519 v
520}
521
522fn collect_cmd_candidates<'a, Ctx: ?Sized>(cmd: &'a CmdSpec<'a, Ctx>) -> Vec<&'a str> {
523 let mut v = Vec::with_capacity(cmd.subs.len());
524 for s in &cmd.subs {
525 if let Some(n) = s.name {
526 v.push(n);
527 }
528 for &al in &s.aliases {
529 v.push(al);
530 }
531 }
532 v
533}
534
535fn suggest_longs<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, name: &str) -> Vec<String> {
536 let thr = max_distance_for(name.len());
537 let mut scored: Vec<(usize, String)> = collect_long_candidates(env, cmd)
538 .into_iter()
539 .map(|cand| (lev(name, &cand), format!("--{cand}")))
540 .collect();
541 scored.sort_by_key(|(d, _)| *d);
542 scored.into_iter().take_while(|(d, _)| *d <= thr).take(3).map(|(_, s)| s).collect()
543}
544
545fn suggest_shorts<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, ch: char) -> Vec<String> {
546 let mut v: Vec<(usize, String)> = collect_short_candidates(env, cmd)
547 .into_iter()
548 .map(|c| (usize::from(c != ch), format!("-{c}")))
549 .collect();
550 v.sort_by_key(|(d, _)| *d);
551 v.into_iter().take_while(|(d, _)| *d <= 1).take(3).map(|(_, s)| s).collect()
552}
553
554fn suggest_cmds<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, tok: &str) -> Vec<String> {
555 let thr = max_distance_for(tok.len());
556 let mut v: Vec<(usize, String)> = collect_cmd_candidates(cmd)
557 .into_iter()
558 .map(|cand| (lev(tok, cand), cand.to_string()))
559 .collect();
560 v.sort_by_key(|(d, _)| *d);
561 v.into_iter().take_while(|(d, _)| *d <= thr).take(3).map(|(_, s)| s).collect()
562}
563
564#[non_exhaustive]
566#[derive(Debug)]
567pub enum Error {
569 MissingValue(String),
571 UnexpectedArgument(String),
573 UnknownOption {
575 token: String,
577 suggestions: Vec<String>,
579 },
580 UnknownCommand {
582 token: String,
584 suggestions: Vec<String>,
586 },
587 GroupViolation(String),
589 MissingPositional(String),
591 TooManyPositional(String),
593 Callback(BoxError),
595 Exit(i32),
597 User(&'static str),
599}
600impl fmt::Display for Error {
601 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
602 match self {
603 Self::UnknownOption { token, suggestions } => {
604 write!(f, "unknown option: '{token}'")?;
605 if !suggestions.is_empty() {
606 write!(f, ". Did you mean {}?", format_alternates(suggestions))?;
607 }
608 Ok(())
609 }
610 Self::MissingValue(n) => write!(f, "missing value for --{n}"),
611 Self::UnexpectedArgument(s) => write!(f, "unexpected argument: {s}"),
612 Self::UnknownCommand { token, suggestions } => {
613 write!(f, "unknown command: {token}")?;
614 if !suggestions.is_empty() {
615 write!(f, ". Did you mean {}?", format_alternates(suggestions))?;
616 }
617 Ok(())
618 }
619 Self::GroupViolation(s) => write!(f, "{s}"),
620 Self::MissingPositional(n) => write!(f, "missing positional: {n}"),
621 Self::TooManyPositional(n) => write!(f, "too many values for: {n}"),
622 Self::Callback(e) => write!(f, "{e}"),
623 Self::Exit(code) => write!(f, "exit {code}"),
624 Self::User(s) => write!(f, "{s}"),
625 }
626 }
627}
628impl std::error::Error for Error {}
629
630fn find_sub<'a, Ctx: ?Sized>(
632 cmd: &'a CmdSpec<'a, Ctx>,
633 name: &str,
634) -> Option<&'a CmdSpec<'a, Ctx>> {
635 for c in &cmd.subs {
636 if let Some(n) = c.name {
637 if n == name {
638 return Some(c);
639 }
640 }
641 if c.aliases.contains(&name) {
642 return Some(c);
643 }
644 }
645 None
646}
647fn apply_env_and_defaults<Ctx: ?Sized>(
648 cmd: &CmdSpec<'_, Ctx>,
649 context: &mut Ctx,
650 counts: &mut [u8],
651) -> Result<()> {
652 if cmd.opts.is_empty() {
653 return Ok(());
654 }
655 if !any_env_or_default(cmd) {
656 return Ok(());
657 }
658
659 for (i, o) in cmd.opts.iter().enumerate() {
661 if let Some(key) = o.env {
662 if let Ok(val) = std::env::var(key) {
663 counts[i] = counts[i].saturating_add(1);
664 (o.cb)(Some(val.as_str()), context).map_err(Error::Callback)?;
665 }
666 }
667 }
668 for (i, o) in cmd.opts.iter().enumerate() {
670 if counts[i] != 0 {
671 continue;
672 }
673 let Some(def) = o.default else { continue };
674 if o.group_id != 0 {
675 let gid = o.group_id;
676 let mut taken = false;
677 for (j, p) in cmd.opts.iter().enumerate() {
678 if p.group_id == gid && counts[j] != 0 {
679 taken = true;
680 break;
681 }
682 }
683 if taken {
684 continue;
685 }
686 }
687 counts[i] = counts[i].saturating_add(1);
688 (o.cb)(Some(def), context).map_err(Error::Callback)?;
689 }
690 Ok(())
691}
692
693#[allow(clippy::too_many_arguments)]
694fn parse_long<Ctx: ?Sized, W: std::io::Write>(
695 env: &Env<'_>,
696 cmd: &CmdSpec<'_, Ctx>,
697 tok: &str,
698 idx: &mut usize,
699 argv: &[&str],
700 context: &mut Ctx,
701 counts: &mut [u8],
702 out: &mut W,
703 chain: &[&str],
704) -> Result<()> {
705 let s = &tok[2..];
707 let (name, attached) = s
708 .as_bytes()
709 .iter()
710 .position(|&b| b == b'=')
711 .map_or((s, None), |eq| (&s[..eq], Some(&s[eq + 1..])));
712 if env.auto_help && name == "help" {
714 print_help_to(env, cmd, chain, out);
715 return Err(Error::Exit(0));
716 }
717 if cmd.name.is_none() {
718 if env.version.is_some() && name == "version" {
719 print_version_to(env, out);
720 return Err(Error::Exit(0));
721 }
722 if env.author.is_some() && name == "author" {
723 print_author_to(env, out);
724 return Err(Error::Exit(0));
725 }
726 }
727 let (i, spec) = match cmd.opts.iter().enumerate().find(|(_, o)| o.name == name) {
728 Some(x) => x,
729 None => return Err(unknown_long_error(env, cmd, name)),
730 };
731 counts[i] = counts[i].saturating_add(1);
732 match spec.arg {
733 ArgKind::None => {
734 (spec.cb)(None, context).map_err(Error::Callback)?;
735 }
736 ArgKind::Required => {
737 let v = if let Some(a) = attached {
738 if a.is_empty() {
739 return Err(Error::MissingValue(spec.name.to_string()));
740 }
741 a
742 } else {
743 take_next(idx, argv).ok_or_else(|| Error::MissingValue(spec.name.to_string()))?
744 };
745 (spec.cb)(Some(v), context).map_err(Error::Callback)?;
746 }
747 ArgKind::Optional => {
748 let v = match (attached, argv.get(*idx).copied()) {
749 (Some(a), _) => Some(a),
750 (None, Some("-")) => {
751 *idx += 1; None
753 }
754 (None, Some(n))
755 if matches!(spec.value_hint, ValueHint::Number) && is_dash_number(n) =>
756 {
757 *idx += 1;
758 Some(n)
759 }
760 (None, Some(n)) if looks_value_like(n) => {
761 *idx += 1;
762 Some(n)
763 }
764 _ => None,
765 };
766 (spec.cb)(v, context).map_err(Error::Callback)?;
767 }
768 }
769 Ok(())
770}
771
772#[allow(clippy::too_many_arguments)]
773fn parse_short_cluster<Ctx: ?Sized, W: std::io::Write>(
774 env: &Env<'_>,
775 cmd: &CmdSpec<'_, Ctx>,
776 tok: &str,
777 idx: &mut usize,
778 argv: &[&str],
779 context: &mut Ctx,
780 counts: &mut [u8],
781 out: &mut W,
782 chain: &[&str],
783) -> Result<()> {
784 let short_idx = build_short_idx(cmd);
786 let s = &tok[1..];
787 let bytes = s.as_bytes();
788 let mut i = 0usize;
789 while i < bytes.len() {
790 let (ch, adv) = if bytes[i] < 128 {
792 (bytes[i] as char, 1)
793 } else {
794 let c = s[i..].chars().next().unwrap();
795 (c, c.len_utf8())
796 };
797 i += adv;
798
799 if env.auto_help && ch == 'h' {
801 print_help_to(env, cmd, chain, out);
802 return Err(Error::Exit(0));
803 }
804 if cmd.name.is_none() {
805 if env.version.is_some() && ch == 'V' {
806 print_version_to(env, out);
807 return Err(Error::Exit(0));
808 }
809 if env.author.is_some() && ch == 'A' {
810 print_author_to(env, out);
811 return Err(Error::Exit(0));
812 }
813 }
814 let (oi, spec) = match lookup_short(cmd, &short_idx, ch) {
815 Some(x) => x,
816 None => return Err(unknown_short_error(env, cmd, ch)),
817 };
818 counts[oi] = counts[oi].saturating_add(1);
819 match spec.arg {
820 ArgKind::None => {
821 (spec.cb)(None, context).map_err(Error::Callback)?;
822 }
823 ArgKind::Required => {
824 if i < s.len() {
825 let rem = &s[i..];
826 (spec.cb)(Some(rem), context).map_err(Error::Callback)?;
827 return Ok(());
828 }
829 let v = take_next(idx, argv)
830 .ok_or_else(|| Error::MissingValue(spec.name.to_string()))?;
831 (spec.cb)(Some(v), context).map_err(Error::Callback)?;
832 return Ok(());
833 }
834 ArgKind::Optional => {
835 if i < s.len() {
836 let rem = &s[i..];
837 (spec.cb)(Some(rem), context).map_err(Error::Callback)?;
838 return Ok(());
839 }
840 let v = match argv.get(*idx) {
842 Some(&"-") => {
843 *idx += 1;
844 None
845 }
846 Some(n)
848 if matches!(spec.value_hint, ValueHint::Number) && is_dash_number(n) =>
849 {
850 *idx += 1;
851 Some(n)
852 }
853 Some(n) if looks_value_like(n) => {
855 *idx += 1;
856 Some(n)
857 }
858 _ => None,
859 };
860 (spec.cb)(v.map(|v| &**v), context).map_err(Error::Callback)?;
861 return Ok(());
862 }
863 }
864 }
865 Ok(())
866}
867
868#[inline]
869fn any_env_or_default<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>) -> bool {
870 cmd.opts.iter().any(|o| o.env.is_some() || o.default.is_some())
871}
872#[inline]
873fn take_next<'a>(idx: &mut usize, argv: &'a [&'a str]) -> Option<&'a str> {
874 let i = *idx;
875 if i < argv.len() {
876 *idx = i + 1;
877 Some(argv[i])
878 } else {
879 None
880 }
881}
882#[inline]
883fn is_short_like(s: &str) -> bool {
884 let b = s.as_bytes();
885 b.len() >= 2 && b[0] == b'-' && b[1] != b'-'
886}
887#[inline]
888fn is_dash_number(s: &str) -> bool {
889 let b = s.as_bytes();
890 if b.is_empty() || b[0] != b'-' {
891 return false;
892 }
893 if b.len() == 1 {
895 return false;
896 }
897 is_numeric_like(&b[1..])
898}
899#[inline]
900fn looks_value_like(s: &str) -> bool {
901 if !s.starts_with('-') {
902 return true;
903 }
904 if s == "-" {
905 return false;
906 }
907 is_numeric_like(&s.as_bytes()[1..])
908}
909#[inline]
910fn is_numeric_like(b: &[u8]) -> bool {
911 let mut i = 0;
913 let n = b.len();
914 if i < n && b[i] == b'.' {
916 i += 1;
917 }
918 let mut nd = 0;
920 while i < n && (b[i] as char).is_ascii_digit() {
921 i += 1;
922 nd += 1;
923 }
924 if nd == 0 {
925 return false;
926 }
927 if i < n && b[i] == b'.' {
929 i += 1;
930 while i < n && (b[i] as char).is_ascii_digit() {
931 i += 1;
932 }
933 }
934 if i < n && (b[i] == b'e' || b[i] == b'E') {
936 i += 1;
937 if i < n && (b[i] == b'+' || b[i] == b'-') {
938 i += 1;
939 }
940 let mut ed = 0;
941 while i < n && (b[i] as char).is_ascii_digit() {
942 i += 1;
943 ed += 1;
944 }
945 if ed == 0 {
946 return false;
947 }
948 }
949 i == n
950}
951
952fn check_groups<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, counts: &[u8]) -> Result<()> {
953 let opts = &cmd.opts;
954 let opts_len = opts.len();
955 let mut index = 0usize;
956 while index < opts_len {
957 let id = opts[index].group_id;
958 if id != 0 {
959 let mut seen = false;
961 let mut k = 0usize;
962 while k < index {
963 if opts[k].group_id == id {
964 seen = true;
965 break;
966 }
967 k += 1;
968 }
969 if !seen {
970 let mut total = 0u32;
971 let mut xor = false;
972 let mut req = false;
973 let mut j = 0usize;
974 while j < opts_len {
975 let o = &opts[j];
976 if o.group_id == id {
977 total += u32::from(counts[j]);
978 match o.group_mode {
979 GroupMode::Xor => xor = true,
980 GroupMode::ReqOne => req = true,
981 GroupMode::None => {}
982 }
983 if xor && total > 1 {
984 return Err(Error::GroupViolation(group_msg(opts, id, true)));
985 }
986 }
987 j += 1;
988 }
989 if req && total == 0 {
990 return Err(Error::GroupViolation(group_msg(opts, id, false)));
991 }
992 }
993 }
994 index += 1;
995 }
996 Ok(())
997}
998
999#[cold]
1000#[inline(never)]
1001fn group_msg<Ctx: ?Sized>(opts: &[OptSpec<'_, Ctx>], id: u16, xor: bool) -> String {
1002 let mut names = String::new();
1003 for o in opts.iter().filter(|o| o.group_id == id) {
1004 if !names.is_empty() {
1005 names.push_str(" | ");
1006 }
1007 names.push_str(o.name);
1008 }
1009 if xor {
1010 format!("at most one of the following options may be used: {names}")
1011 } else {
1012 format!("one of the following options is required: {names}")
1013 }
1014}
1015
1016fn validate_positionals<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, pos: &[&str]) -> Result<()> {
1017 if cmd.pos.is_empty() {
1018 return Ok(());
1019 }
1020 let total = pos.len();
1021 let mut min_sum: usize = 0;
1022 let mut max_sum: Option<usize> = Some(0);
1023 for p in &cmd.pos {
1024 min_sum = min_sum.saturating_add(p.min);
1025 if let Some(ms) = max_sum {
1026 if p.max == usize::MAX {
1027 max_sum = None;
1028 } else {
1029 max_sum = Some(ms.saturating_add(p.max));
1030 }
1031 }
1032 }
1033 if total < min_sum {
1035 let mut need = 0usize;
1036 for p in &cmd.pos {
1037 need = need.saturating_add(p.min);
1038 if total < need {
1039 return Err(Error::MissingPositional(p.name.to_string()));
1040 }
1041 }
1042 return Err(Error::MissingPositional(
1044 cmd.pos.first().map_or("<args>", |p| p.name).to_string(),
1045 ));
1046 }
1047 if let Some(ms) = max_sum {
1049 if total > ms {
1050 let last = cmd.pos.last().map_or("<args>", |p| p.name);
1051 return Err(Error::TooManyPositional(last.to_string()));
1052 }
1053 }
1054 Ok(())
1055}
1056#[inline]
1057const fn plain_opt_label_len<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> usize {
1058 let mut len = if o.short.is_some() { 4 } else { 0 }; len += 2 + o.name.len(); if let Some(m) = o.metavar {
1061 len += 1 + m.len();
1062 }
1063 len
1064}
1065#[inline]
1066fn make_opt_label<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> String {
1067 let mut s = String::new();
1068 if let Some(ch) = o.short {
1069 s.push('-');
1070 s.push(ch);
1071 s.push(',');
1072 s.push(' ');
1073 }
1074 s.push_str("--");
1075 s.push_str(o.name);
1076 if let Some(m) = o.metavar {
1077 s.push(' ');
1078 s.push_str(m);
1079 }
1080 s
1081}
1082
1083const C_BOLD: &str = "\u{001b}[1m";
1085const C_UNDERLINE: &str = "\u{001b}[4m";
1086const C_BRIGHT_WHITE: &str = "\u{001b}[97m";
1087const C_CYAN: &str = "\u{001b}[36m";
1088const C_MAGENTA: &str = "\u{001b}[35m";
1089const C_YELLOW: &str = "\u{001b}[33m";
1090const C_RESET: &str = "\u{001b}[0m";
1091#[inline]
1092fn colorize(s: &str, color: &str, env: &Env) -> String {
1093 if !env.color || color.is_empty() {
1094 s.to_string()
1095 } else {
1096 format!("{color}{s}{C_RESET}")
1097 }
1098}
1099#[inline]
1100fn help_text_for_opt<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> String {
1101 match (o.env, o.default) {
1102 (Some(k), Some(d)) => format!("{} (env {k}, default={d})", o.help),
1103 (Some(k), None) => format!("{} (env {k})", o.help),
1104 (None, Some(d)) => format!("{} (default={d})", o.help),
1105 (None, None) => o.help.to_string(),
1106 }
1107}
1108#[inline]
1109fn print_header(buf: &mut String, text: &str, env: &Env) {
1110 let _ = writeln!(buf, "\n{}:", colorize(text, &[C_BOLD, C_UNDERLINE].concat(), env).as_str());
1111}
1112#[inline]
1113fn lookup_short<'a, Ctx: ?Sized>(
1114 cmd: &'a CmdSpec<'a, Ctx>,
1115 table: &[u16; 128],
1116 ch: char,
1117) -> Option<(usize, &'a OptSpec<'a, Ctx>)> {
1118 let c = ch as u32;
1119 if c < 128 {
1120 let i = table[c as usize];
1121 if i != u16::MAX {
1122 let idx = i as usize;
1123 return Some((idx, &cmd.opts[idx]));
1124 }
1125 return None;
1126 }
1127 cmd.opts.iter().enumerate().find(|(_, o)| o.short == Some(ch))
1128}
1129fn build_short_idx<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>) -> [u16; 128] {
1130 let mut map = [u16::MAX; 128];
1131 let mut i = 0usize;
1132 let len = cmd.opts.len();
1133 while i < len {
1134 let o = &cmd.opts[i];
1135 if let Some(ch) = o.short {
1136 let cu = ch as usize;
1137 if cu < 128 {
1138 debug_assert!(u16::try_from(i).is_ok());
1139 map[cu] = u16::try_from(i).unwrap_or(0); }
1141 }
1142 i += 1;
1143 }
1144 map
1145}
1146#[inline]
1147fn write_wrapped(buf: &mut String, text: &str, indent_cols: usize, wrap_cols: usize) {
1148 if wrap_cols == 0 {
1149 let _ = writeln!(buf, "{text}");
1150 return;
1151 }
1152 let mut col = indent_cols;
1153 let mut first = true;
1154 for word in text.split_whitespace() {
1155 let wlen = word.len();
1156 if first {
1157 buf.push_str(word);
1158 col = indent_cols + wlen;
1159 first = false;
1160 continue;
1161 }
1162 if col + 1 + wlen > wrap_cols {
1163 buf.push('\n');
1164 for _ in 0..indent_cols {
1165 buf.push(' ');
1166 }
1167 buf.push_str(word);
1168 col = indent_cols + wlen;
1169 } else {
1170 buf.push(' ');
1171 buf.push_str(word);
1172 col += 1 + wlen;
1173 }
1174 }
1175 buf.push('\n');
1176}
1177
1178fn write_row(
1179 buf: &mut String,
1180 env: &Env,
1181 color: &str,
1182 plain_label: &str,
1183 help: &str,
1184 label_col: usize,
1185) {
1186 let _ = write!(buf, " {}", colorize(plain_label, color, env));
1187 let pad = label_col.saturating_sub(plain_label.len());
1188 for _ in 0..pad {
1189 buf.push(' ');
1190 }
1191 buf.push(' ');
1192 buf.push(' ');
1193 let indent = 4 + label_col;
1194 write_wrapped(buf, help, indent, env.wrap_cols);
1195}
1196
1197#[cold]
1199#[inline(never)]
1200pub fn print_help_to<Ctx: ?Sized, W: Write>(
1201 env: &Env<'_>,
1202 cmd: &CmdSpec<'_, Ctx>,
1203 path: &[&str],
1204 mut out: W,
1205) {
1206 let mut buf = String::new();
1207 let _ = write!(
1208 buf,
1209 "Usage: {}",
1210 colorize(env.name, [C_BOLD, C_BRIGHT_WHITE].concat().as_str(), env)
1211 );
1212 for tok in path {
1213 let _ = write!(buf, " {}", colorize(tok, C_MAGENTA, env));
1214 }
1215 let has_subs = !cmd.subs.is_empty();
1216 let has_opts = !cmd.opts.is_empty()
1217 || env.auto_help
1218 || (path.is_empty() && (env.version.is_some() || env.author.is_some()));
1219 if has_opts && has_subs {
1220 let _ = write!(buf, " {}", colorize("[options]", C_CYAN, env));
1221 let _ = write!(buf, " {}", colorize("<command>", C_MAGENTA, env));
1222 } else if !has_opts && has_subs {
1223 let _ = write!(buf, " {}", colorize("<command>", C_MAGENTA, env));
1224 } else if has_opts && !has_subs {
1225 let _ = write!(buf, " {}", colorize("[options]", C_CYAN, env));
1226 }
1227 for p in &cmd.pos {
1228 if p.min == 0 {
1229 let _ = write!(buf, " [{}]", colorize(p.name, C_YELLOW, env));
1230 } else if p.min == 1 && p.max == 1 {
1231 let _ = write!(buf, " {}", colorize(p.name, C_YELLOW, env));
1232 } else if p.max > 1 {
1233 let _ = write!(buf, " {}...", colorize(p.name, C_YELLOW, env));
1234 }
1235 }
1236 let _ = writeln!(buf);
1237 if let Some(desc) = cmd.desc {
1238 let _ = writeln!(buf, "\n{desc}");
1239 }
1240 if !cmd.opts.is_empty()
1241 || env.auto_help
1242 || (cmd.name.is_none() && (env.version.is_some() || env.author.is_some()))
1243 {
1244 print_header(&mut buf, "Options", env);
1245 let mut width = 0usize;
1246 if env.auto_help {
1247 width = width.max("-h, --help".len());
1248 }
1249 if cmd.name.is_none() {
1251 if env.version.is_some() {
1252 width = width.max("-V, --version".len());
1253 }
1254 if env.author.is_some() {
1255 width = width.max("-A, --author".len());
1256 }
1257 }
1258 for o in &cmd.opts {
1259 width = width.max(plain_opt_label_len(o));
1260 }
1261 if env.auto_help {
1262 write_row(&mut buf, env, C_CYAN, "-h, --help", "Show this help and exit", width);
1263 }
1264 if cmd.name.is_none() {
1266 if env.version.is_some() {
1267 write_row(&mut buf, env, C_CYAN, "-V, --version", "Show version and exit", width);
1268 }
1269 if env.author.is_some() {
1270 write_row(&mut buf, env, C_CYAN, "--author", "Show author and exit", width);
1271 }
1272 }
1273 for o in &cmd.opts {
1274 let label = make_opt_label(o);
1275 let help = help_text_for_opt(o);
1276 write_row(&mut buf, env, C_CYAN, &label, &help, width);
1277 }
1278 }
1279 if !cmd.subs.is_empty() {
1281 print_header(&mut buf, "Commands", env);
1282 let width = cmd.subs.iter().map(|s| s.name.unwrap_or("<root>").len()).max().unwrap_or(0);
1283 for s in &cmd.subs {
1284 let name = s.name.unwrap_or("<root>");
1285 write_row(&mut buf, env, C_MAGENTA, name, s.desc.unwrap_or(""), width);
1286 }
1287 }
1288 if !cmd.pos.is_empty() {
1290 print_header(&mut buf, "Positionals", env);
1291 let width = cmd.pos.iter().map(|p| p.name.len()).max().unwrap_or(0);
1292 for p in &cmd.pos {
1293 let help = help_for_pos(p);
1294 write_row(&mut buf, env, C_YELLOW, p.name, &help, width);
1295 }
1296 }
1297 let _ = out.write_all(buf.as_bytes());
1298}
1299fn help_for_pos(p: &PosSpec) -> String {
1300 if let Some(d) = p.desc {
1301 return d.to_string();
1302 }
1303 if p.min == 0 {
1304 return "(optional)".to_string();
1305 }
1306 if p.min == 1 && p.max == 1 {
1307 return "(required)".to_string();
1308 }
1309 if p.min == 1 {
1310 return "(at least one required)".to_string();
1311 }
1312 format!("min={} max={}", p.min, p.max)
1313}
1314#[cold]
1316#[inline(never)]
1317pub fn print_version_to<W: Write>(env: &Env<'_>, mut out: W) {
1318 if let Some(v) = env.version {
1319 let _ = writeln!(out, "{v}");
1320 }
1321}
1322#[cold]
1324#[inline(never)]
1325pub fn print_author_to<W: Write>(env: &Env<'_>, mut out: W) {
1326 if let Some(a) = env.author {
1327 let _ = writeln!(out, "{a}");
1328 }
1329}
1330#[cold]
1331#[inline(never)]
1332fn unknown_long_error<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, name: &str) -> Error {
1333 let token = {
1334 let mut s = String::with_capacity(2 + name.len());
1335 s.push_str("--");
1336 s.push_str(name);
1337 s
1338 };
1339 let suggestions = suggest_longs(env, cmd, name);
1340 Error::UnknownOption { token, suggestions }
1341}
1342
1343#[cold]
1344#[inline(never)]
1345fn unknown_short_error<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, ch: char) -> Error {
1346 let mut token = String::with_capacity(2);
1347 token.push('-');
1348 token.push(ch);
1349 let suggestions = suggest_shorts(env, cmd, ch);
1350 Error::UnknownOption { token, suggestions }
1351}
1352
1353#[cold]
1354#[inline(never)]
1355fn unknown_command_error<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, tok: &str) -> Error {
1356 Error::UnknownCommand { token: tok.to_string(), suggestions: suggest_cmds(cmd, tok) }
1357}