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;
338 while idx < argv.len() {
339 if let Some(next) = find_sub(cmd, argv[idx]) {
340 cmd = next;
341 idx += 1;
342 } else {
343 break;
344 }
345 }
346 if !cmd.subs.is_empty() && cmd.pos.is_empty() && idx < argv.len() {
349 let tok = argv[idx];
350 if !tok.starts_with('-') && tok != "--" && find_sub(cmd, tok).is_none() {
351 return Err(unknown_command_error(cmd, tok));
352 }
353 }
354 let mut gcounts: Vec<u8> = vec![0; cmd.opts.len()];
356 let mut pos: Vec<&str> = Vec::with_capacity(argv.len().saturating_sub(idx));
358 let mut stop_opts = false;
359 while idx < argv.len() {
360 let tok = argv[idx];
361 if !stop_opts {
362 if tok == "--" {
363 stop_opts = true;
364 idx += 1;
365 continue;
366 }
367 if tok.starts_with("--") {
368 idx += 1;
369 parse_long(env, cmd, tok, &mut idx, argv, context, &mut gcounts, out)?;
370 continue;
371 }
372 if is_short_like(tok) {
373 idx += 1;
374 parse_short_cluster(env, cmd, tok, &mut idx, argv, context, &mut gcounts, out)?;
375 continue;
376 }
377 }
378 pos.push(tok);
379 idx += 1;
380 }
381 if cmd.pos.is_empty() && !pos.is_empty() {
383 return Err(Error::UnexpectedArgument(pos[0].to_string()));
384 }
385 apply_env_and_defaults(cmd, context, &mut gcounts)?;
386 check_groups(cmd, &gcounts)?;
388 validate_positionals(cmd, &pos)?;
390 if let Some(run) = cmd.run {
392 return run(&pos, context).map_err(Error::Callback);
393 }
394 Ok(())
395}
396
397pub fn dispatch<Ctx>(
401 env: &Env<'_>,
402 root: &CmdSpec<'_, Ctx>,
403 argv: &[&str],
404 context: &mut Ctx,
405) -> Result<()> {
406 let mut out = io::stdout();
407 dispatch_to(env, root, argv, context, &mut out)
408}
409
410#[inline]
412fn format_alternates(items: &[String]) -> String {
413 match items.len() {
414 0 => String::new(),
415 1 => format!("'{}'", items[0]),
416 2 => format!("'{}' or '{}'", items[0], items[1]),
417 _ => {
418 let mut s = String::new();
420 for (i, it) in items.iter().enumerate() {
421 if i > 0 {
422 s.push_str(if i + 1 == items.len() { ", or " } else { ", " });
423 }
424 s.push('\'');
425 s.push_str(it);
426 s.push('\'');
427 }
428 s
429 }
430 }
431}
432
433#[inline]
434const fn max_distance_for(len: usize) -> usize {
435 match len {
436 0..=3 => 1,
437 4..=6 => 2,
438 _ => 3,
439 }
440}
441
442fn lev(a: &str, b: &str) -> usize {
444 let (na, nb) = (a.len(), b.len());
445 if na == 0 {
446 return nb;
447 }
448 if nb == 0 {
449 return na;
450 }
451 let mut prev: Vec<usize> = (0..=nb).collect();
452 let mut curr = vec![0; nb + 1];
453 for (i, ca) in a.chars().enumerate() {
454 curr[0] = i + 1;
455 for (j, cb) in b.chars().enumerate() {
456 let cost = usize::from(ca != cb);
457 curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
458 }
459 std::mem::swap(&mut prev, &mut curr);
460 }
461 prev[nb]
462}
463
464fn collect_long_candidates<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>) -> Vec<String> {
465 let mut v = Vec::with_capacity(cmd.opts.len() + 3);
466 if env.auto_help {
467 v.push("help".to_string());
468 }
469 if env.version.is_some() {
470 v.push("version".to_string());
471 }
472 if env.author.is_some() {
473 v.push("author".to_string());
474 }
475 for o in &cmd.opts {
476 v.push(o.name.to_string());
477 }
478 v
479}
480
481fn collect_short_candidates<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>) -> Vec<char> {
482 let mut v = Vec::with_capacity(cmd.opts.len() + 3);
483 if env.auto_help {
484 v.push('h');
485 }
486 if env.version.is_some() {
487 v.push('V');
488 }
489 if env.author.is_some() {
490 v.push('A');
491 }
492 for o in &cmd.opts {
493 if let Some(ch) = o.short {
494 v.push(ch);
495 }
496 }
497 v
498}
499
500fn collect_cmd_candidates<'a, Ctx: ?Sized>(cmd: &'a CmdSpec<'a, Ctx>) -> Vec<&'a str> {
501 let mut v = Vec::with_capacity(cmd.subs.len());
502 for s in &cmd.subs {
503 if let Some(n) = s.name {
504 v.push(n);
505 }
506 for &al in &s.aliases {
507 v.push(al);
508 }
509 }
510 v
511}
512
513fn suggest_longs<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, name: &str) -> Vec<String> {
514 let thr = max_distance_for(name.len());
515 let mut scored: Vec<(usize, String)> = collect_long_candidates(env, cmd)
516 .into_iter()
517 .map(|cand| (lev(name, &cand), format!("--{cand}")))
518 .collect();
519 scored.sort_by_key(|(d, _)| *d);
520 scored.into_iter().take_while(|(d, _)| *d <= thr).take(3).map(|(_, s)| s).collect()
521}
522
523fn suggest_shorts<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, ch: char) -> Vec<String> {
524 let mut v: Vec<(usize, String)> = collect_short_candidates(env, cmd)
525 .into_iter()
526 .map(|c| (usize::from(c != ch), format!("-{c}")))
527 .collect();
528 v.sort_by_key(|(d, _)| *d);
529 v.into_iter().take_while(|(d, _)| *d <= 1).take(3).map(|(_, s)| s).collect()
530}
531
532fn suggest_cmds<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, tok: &str) -> Vec<String> {
533 let thr = max_distance_for(tok.len());
534 let mut v: Vec<(usize, String)> = collect_cmd_candidates(cmd)
535 .into_iter()
536 .map(|cand| (lev(tok, cand), cand.to_string()))
537 .collect();
538 v.sort_by_key(|(d, _)| *d);
539 v.into_iter().take_while(|(d, _)| *d <= thr).take(3).map(|(_, s)| s).collect()
540}
541
542#[non_exhaustive]
544#[derive(Debug)]
545pub enum Error {
547 MissingValue(String),
549 UnexpectedArgument(String),
551 UnknownOption {
553 token: String,
555 suggestions: Vec<String>,
557 },
558 UnknownCommand {
560 token: String,
562 suggestions: Vec<String>,
564 },
565 GroupViolation(String),
567 MissingPositional(String),
569 TooManyPositional(String),
571 Callback(BoxError),
573 Exit(i32),
575 User(&'static str),
577}
578impl fmt::Display for Error {
579 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580 match self {
581 Self::UnknownOption { token, suggestions } => {
582 write!(f, "unknown option: '{token}'")?;
583 if !suggestions.is_empty() {
584 write!(f, ". Did you mean {}?", format_alternates(suggestions))?;
585 }
586 Ok(())
587 }
588 Self::MissingValue(n) => write!(f, "missing value for --{n}"),
589 Self::UnexpectedArgument(s) => write!(f, "unexpected argument: {s}"),
590 Self::UnknownCommand { token, suggestions } => {
591 write!(f, "unknown command: {token}")?;
592 if !suggestions.is_empty() {
593 write!(f, ". Did you mean {}?", format_alternates(suggestions))?;
594 }
595 Ok(())
596 }
597 Self::GroupViolation(s) => write!(f, "{s}"),
598 Self::MissingPositional(n) => write!(f, "missing positional: {n}"),
599 Self::TooManyPositional(n) => write!(f, "too many values for: {n}"),
600 Self::Callback(e) => write!(f, "{e}"),
601 Self::Exit(code) => write!(f, "exit {code}"),
602 Self::User(s) => write!(f, "{s}"),
603 }
604 }
605}
606impl std::error::Error for Error {}
607
608fn find_sub<'a, Ctx: ?Sized>(
610 cmd: &'a CmdSpec<'a, Ctx>,
611 name: &str,
612) -> Option<&'a CmdSpec<'a, Ctx>> {
613 for c in &cmd.subs {
614 if let Some(n) = c.name {
615 if n == name {
616 return Some(c);
617 }
618 }
619 if c.aliases.contains(&name) {
620 return Some(c);
621 }
622 }
623 None
624}
625fn apply_env_and_defaults<Ctx: ?Sized>(
626 cmd: &CmdSpec<'_, Ctx>,
627 context: &mut Ctx,
628 counts: &mut [u8],
629) -> Result<()> {
630 if cmd.opts.is_empty() {
631 return Ok(());
632 }
633 if !any_env_or_default(cmd) {
634 return Ok(());
635 }
636
637 for (i, o) in cmd.opts.iter().enumerate() {
639 if let Some(key) = o.env {
640 if let Ok(val) = std::env::var(key) {
641 counts[i] = counts[i].saturating_add(1);
642 (o.cb)(Some(val.as_str()), context).map_err(Error::Callback)?;
643 }
644 }
645 }
646 for (i, o) in cmd.opts.iter().enumerate() {
648 if counts[i] != 0 {
649 continue;
650 }
651 let Some(def) = o.default else { continue };
652 if o.group_id != 0 {
653 let gid = o.group_id;
654 let mut taken = false;
655 for (j, p) in cmd.opts.iter().enumerate() {
656 if p.group_id == gid && counts[j] != 0 {
657 taken = true;
658 break;
659 }
660 }
661 if taken {
662 continue;
663 }
664 }
665 counts[i] = counts[i].saturating_add(1);
666 (o.cb)(Some(def), context).map_err(Error::Callback)?;
667 }
668 Ok(())
669}
670
671#[allow(clippy::too_many_arguments)]
672fn parse_long<Ctx: ?Sized, W: std::io::Write>(
673 env: &Env<'_>,
674 cmd: &CmdSpec<'_, Ctx>,
675 tok: &str,
676 idx: &mut usize,
677 argv: &[&str],
678 context: &mut Ctx,
679 counts: &mut [u8],
680 out: &mut W,
681) -> Result<()> {
682 let s = &tok[2..];
684 let (name, attached) = s
685 .as_bytes()
686 .iter()
687 .position(|&b| b == b'=')
688 .map_or((s, None), |eq| (&s[..eq], Some(&s[eq + 1..])));
689 if env.auto_help && name == "help" {
691 print_help_to(env, cmd, out);
692 return Err(Error::Exit(0));
693 }
694 if env.version.is_some() && name == "version" {
695 print_version_to(env, out);
696 return Err(Error::Exit(0));
697 }
698 if env.author.is_some() && name == "author" {
699 print_author_to(env, out);
700 return Err(Error::Exit(0));
701 }
702 let (i, spec) = match cmd.opts.iter().enumerate().find(|(_, o)| o.name == name) {
703 Some(x) => x,
704 None => return Err(unknown_long_error(env, cmd, name)),
705 };
706 counts[i] = counts[i].saturating_add(1);
707 match spec.arg {
708 ArgKind::None => {
709 (spec.cb)(None, context).map_err(Error::Callback)?;
710 }
711 ArgKind::Required => {
712 let v = if let Some(a) = attached {
713 if a.is_empty() {
714 return Err(Error::MissingValue(spec.name.to_string()));
715 }
716 a
717 } else {
718 take_next(idx, argv).ok_or_else(|| Error::MissingValue(spec.name.to_string()))?
719 };
720 (spec.cb)(Some(v), context).map_err(Error::Callback)?;
721 }
722 ArgKind::Optional => {
723 let v = match (attached, argv.get(*idx).copied()) {
724 (Some(a), _) => Some(a),
725 (None, Some("-")) => {
726 *idx += 1; None
728 }
729 (None, Some(n))
730 if matches!(spec.value_hint, ValueHint::Number) && is_dash_number(n) =>
731 {
732 *idx += 1;
733 Some(n)
734 }
735 (None, Some(n)) if looks_value_like(n) => {
736 *idx += 1;
737 Some(n)
738 }
739 _ => None,
740 };
741 (spec.cb)(v, context).map_err(Error::Callback)?;
742 }
743 }
744 Ok(())
745}
746
747#[allow(clippy::too_many_arguments)]
748fn parse_short_cluster<Ctx: ?Sized, W: std::io::Write>(
749 env: &Env<'_>,
750 cmd: &CmdSpec<'_, Ctx>,
751 tok: &str,
752 idx: &mut usize,
753 argv: &[&str],
754 context: &mut Ctx,
755 counts: &mut [u8],
756 out: &mut W,
757) -> Result<()> {
758 let short_idx = build_short_idx(cmd);
760 let s = &tok[1..];
761 let bytes = s.as_bytes();
762 let mut i = 0usize;
763 while i < bytes.len() {
764 let (ch, adv) = if bytes[i] < 128 {
766 (bytes[i] as char, 1)
767 } else {
768 let c = s[i..].chars().next().unwrap();
769 (c, c.len_utf8())
770 };
771 i += adv;
772
773 if env.auto_help && ch == 'h' {
775 print_help_to(env, cmd, out);
776 return Err(Error::Exit(0));
777 }
778 if env.version.is_some() && ch == 'V' {
779 print_version_to(env, out);
780 return Err(Error::Exit(0));
781 }
782 if env.author.is_some() && ch == 'A' {
783 print_author_to(env, out);
784 return Err(Error::Exit(0));
785 }
786 let (oi, spec) = match lookup_short(cmd, &short_idx, ch) {
787 Some(x) => x,
788 None => return Err(unknown_short_error(env, cmd, ch)),
789 };
790 counts[oi] = counts[oi].saturating_add(1);
791 match spec.arg {
792 ArgKind::None => {
793 (spec.cb)(None, context).map_err(Error::Callback)?;
794 }
795 ArgKind::Required => {
796 if i < s.len() {
797 let rem = &s[i..];
798 (spec.cb)(Some(rem), context).map_err(Error::Callback)?;
799 return Ok(());
800 }
801 let v = take_next(idx, argv)
802 .ok_or_else(|| Error::MissingValue(spec.name.to_string()))?;
803 (spec.cb)(Some(v), context).map_err(Error::Callback)?;
804 return Ok(());
805 }
806 ArgKind::Optional => {
807 if i < s.len() {
808 let rem = &s[i..];
809 (spec.cb)(Some(rem), context).map_err(Error::Callback)?;
810 return Ok(());
811 }
812 let v = match argv.get(*idx) {
814 Some(&"-") => {
815 *idx += 1;
816 None
817 }
818 Some(n)
820 if matches!(spec.value_hint, ValueHint::Number) && is_dash_number(n) =>
821 {
822 *idx += 1;
823 Some(n)
824 }
825 Some(n) if looks_value_like(n) => {
827 *idx += 1;
828 Some(n)
829 }
830 _ => None,
831 };
832 (spec.cb)(v.map(|v| &**v), context).map_err(Error::Callback)?;
833 return Ok(());
834 }
835 }
836 }
837 Ok(())
838}
839
840#[inline]
841fn any_env_or_default<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>) -> bool {
842 cmd.opts.iter().any(|o| o.env.is_some() || o.default.is_some())
843}
844#[inline]
845fn take_next<'a>(idx: &mut usize, argv: &'a [&'a str]) -> Option<&'a str> {
846 let i = *idx;
847 if i < argv.len() {
848 *idx = i + 1;
849 Some(argv[i])
850 } else {
851 None
852 }
853}
854#[inline]
855fn is_short_like(s: &str) -> bool {
856 let b = s.as_bytes();
857 b.len() >= 2 && b[0] == b'-' && b[1] != b'-'
858}
859#[inline]
860fn is_dash_number(s: &str) -> bool {
861 let b = s.as_bytes();
862 if b.is_empty() || b[0] != b'-' {
863 return false;
864 }
865 if b.len() == 1 {
867 return false;
868 }
869 is_numeric_like(&b[1..])
870}
871#[inline]
872fn looks_value_like(s: &str) -> bool {
873 if !s.starts_with('-') {
874 return true;
875 }
876 if s == "-" {
877 return false;
878 }
879 is_numeric_like(&s.as_bytes()[1..])
880}
881#[inline]
882fn is_numeric_like(b: &[u8]) -> bool {
883 let mut i = 0;
885 let n = b.len();
886 if i < n && b[i] == b'.' {
888 i += 1;
889 }
890 let mut nd = 0;
892 while i < n && (b[i] as char).is_ascii_digit() {
893 i += 1;
894 nd += 1;
895 }
896 if nd == 0 {
897 return false;
898 }
899 if i < n && b[i] == b'.' {
901 i += 1;
902 while i < n && (b[i] as char).is_ascii_digit() {
903 i += 1;
904 }
905 }
906 if i < n && (b[i] == b'e' || b[i] == b'E') {
908 i += 1;
909 if i < n && (b[i] == b'+' || b[i] == b'-') {
910 i += 1;
911 }
912 let mut ed = 0;
913 while i < n && (b[i] as char).is_ascii_digit() {
914 i += 1;
915 ed += 1;
916 }
917 if ed == 0 {
918 return false;
919 }
920 }
921 i == n
922}
923
924fn check_groups<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, counts: &[u8]) -> Result<()> {
925 let opts = &cmd.opts;
926 let opts_len = opts.len();
927 let mut index = 0usize;
928 while index < opts_len {
929 let id = opts[index].group_id;
930 if id != 0 {
931 let mut seen = false;
933 let mut k = 0usize;
934 while k < index {
935 if opts[k].group_id == id {
936 seen = true;
937 break;
938 }
939 k += 1;
940 }
941 if !seen {
942 let mut total = 0u32;
943 let mut xor = false;
944 let mut req = false;
945 let mut j = 0usize;
946 while j < opts_len {
947 let o = &opts[j];
948 if o.group_id == id {
949 total += u32::from(counts[j]);
950 match o.group_mode {
951 GroupMode::Xor => xor = true,
952 GroupMode::ReqOne => req = true,
953 GroupMode::None => {}
954 }
955 if xor && total > 1 {
956 return Err(Error::GroupViolation(group_msg(opts, id, true)));
957 }
958 }
959 j += 1;
960 }
961 if req && total == 0 {
962 return Err(Error::GroupViolation(group_msg(opts, id, false)));
963 }
964 }
965 }
966 index += 1;
967 }
968 Ok(())
969}
970
971#[cold]
972#[inline(never)]
973fn group_msg<Ctx: ?Sized>(opts: &[OptSpec<'_, Ctx>], id: u16, xor: bool) -> String {
974 let mut names = String::new();
975 for o in opts.iter().filter(|o| o.group_id == id) {
976 if !names.is_empty() {
977 names.push_str(" | ");
978 }
979 names.push_str(o.name);
980 }
981 if xor {
982 format!("at most one of the following options may be used: {names}")
983 } else {
984 format!("one of the following options is required: {names}")
985 }
986}
987
988fn validate_positionals<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, pos: &[&str]) -> Result<()> {
989 if cmd.pos.is_empty() {
990 return Ok(());
991 }
992 let total = pos.len();
993 let mut min_sum: usize = 0;
994 let mut max_sum: Option<usize> = Some(0);
995 for p in &cmd.pos {
996 min_sum = min_sum.saturating_add(p.min);
997 if let Some(ms) = max_sum {
998 if p.max == usize::MAX {
999 max_sum = None;
1000 } else {
1001 max_sum = Some(ms.saturating_add(p.max));
1002 }
1003 }
1004 }
1005 if total < min_sum {
1007 let mut need = 0usize;
1008 for p in &cmd.pos {
1009 need = need.saturating_add(p.min);
1010 if total < need {
1011 return Err(Error::MissingPositional(p.name.to_string()));
1012 }
1013 }
1014 return Err(Error::MissingPositional(
1016 cmd.pos.first().map_or("<args>", |p| p.name).to_string(),
1017 ));
1018 }
1019 if let Some(ms) = max_sum {
1021 if total > ms {
1022 let last = cmd.pos.last().map_or("<args>", |p| p.name);
1023 return Err(Error::TooManyPositional(last.to_string()));
1024 }
1025 }
1026 Ok(())
1027}
1028#[inline]
1029const fn plain_opt_label_len<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> usize {
1030 let mut len = if o.short.is_some() { 4 } else { 0 }; len += 2 + o.name.len(); if let Some(m) = o.metavar {
1033 len += 1 + m.len();
1034 }
1035 len
1036}
1037#[inline]
1038fn make_opt_label<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> String {
1039 let mut s = String::new();
1040 if let Some(ch) = o.short {
1041 s.push('-');
1042 s.push(ch);
1043 s.push(',');
1044 s.push(' ');
1045 }
1046 s.push_str("--");
1047 s.push_str(o.name);
1048 if let Some(m) = o.metavar {
1049 s.push(' ');
1050 s.push_str(m);
1051 }
1052 s
1053}
1054
1055const C_BOLD: &str = "\u{001b}[1m";
1057const C_UNDERLINE: &str = "\u{001b}[4m";
1058const C_BRIGHT_WHITE: &str = "\u{001b}[97m";
1059const C_CYAN: &str = "\u{001b}[36m";
1060const C_MAGENTA: &str = "\u{001b}[35m";
1061const C_YELLOW: &str = "\u{001b}[33m";
1062const C_RESET: &str = "\u{001b}[0m";
1063#[inline]
1064fn colorize(s: &str, color: &str, env: &Env) -> String {
1065 if !env.color || color.is_empty() {
1066 s.to_string()
1067 } else {
1068 format!("{color}{s}{C_RESET}")
1069 }
1070}
1071#[inline]
1072fn help_text_for_opt<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> String {
1073 match (o.env, o.default) {
1074 (Some(k), Some(d)) => format!("{} (env {k}, default={d})", o.help),
1075 (Some(k), None) => format!("{} (env {k})", o.help),
1076 (None, Some(d)) => format!("{} (default={d})", o.help),
1077 (None, None) => o.help.to_string(),
1078 }
1079}
1080#[inline]
1081fn print_header(buf: &mut String, text: &str, env: &Env) {
1082 let _ = writeln!(buf, "\n{}:", colorize(text, &[C_BOLD, C_UNDERLINE].concat(), env).as_str());
1083}
1084#[inline]
1085fn lookup_short<'a, Ctx: ?Sized>(
1086 cmd: &'a CmdSpec<'a, Ctx>,
1087 table: &[u16; 128],
1088 ch: char,
1089) -> Option<(usize, &'a OptSpec<'a, Ctx>)> {
1090 let c = ch as u32;
1091 if c < 128 {
1092 let i = table[c as usize];
1093 if i != u16::MAX {
1094 let idx = i as usize;
1095 return Some((idx, &cmd.opts[idx]));
1096 }
1097 return None;
1098 }
1099 cmd.opts.iter().enumerate().find(|(_, o)| o.short == Some(ch))
1100}
1101fn build_short_idx<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>) -> [u16; 128] {
1102 let mut map = [u16::MAX; 128];
1103 let mut i = 0usize;
1104 let len = cmd.opts.len();
1105 while i < len {
1106 let o = &cmd.opts[i];
1107 if let Some(ch) = o.short {
1108 let cu = ch as usize;
1109 if cu < 128 {
1110 debug_assert!(u16::try_from(i).is_ok());
1111 map[cu] = u16::try_from(i).unwrap_or(0); }
1113 }
1114 i += 1;
1115 }
1116 map
1117}
1118#[inline]
1119fn write_wrapped(buf: &mut String, text: &str, indent_cols: usize, wrap_cols: usize) {
1120 if wrap_cols == 0 {
1121 let _ = writeln!(buf, "{text}");
1122 return;
1123 }
1124 let mut col = indent_cols;
1125 let mut first = true;
1126 for word in text.split_whitespace() {
1127 let wlen = word.len();
1128 if first {
1129 for _ in 0..indent_cols {
1130 buf.push(' ');
1131 }
1132 buf.push_str(word);
1133 col = indent_cols + wlen;
1134 first = false;
1135 continue;
1136 }
1137 if col + 1 + wlen > wrap_cols {
1138 buf.push('\n');
1139 for _ in 0..indent_cols {
1140 buf.push(' ');
1141 }
1142 buf.push_str(word);
1143 col = indent_cols + wlen;
1144 } else {
1145 buf.push(' ');
1146 buf.push_str(word);
1147 col += 1 + wlen;
1148 }
1149 }
1150 buf.push('\n');
1151}
1152
1153fn write_row(
1154 buf: &mut String,
1155 env: &Env,
1156 color: &str,
1157 plain_label: &str,
1158 help: &str,
1159 label_col: usize,
1160) {
1161 let _ = write!(buf, " {}", colorize(plain_label, color, env));
1162 let pad = label_col.saturating_sub(plain_label.len());
1163 for _ in 0..pad {
1164 buf.push(' ');
1165 }
1166 buf.push(' ');
1167 buf.push(' ');
1168 let indent = 4 + label_col;
1169 write_wrapped(buf, help, indent, env.wrap_cols);
1170}
1171
1172#[cold]
1174#[inline(never)]
1175pub fn print_help_to<Ctx: ?Sized, W: Write>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, mut out: W) {
1176 let mut buf = String::new();
1177 let _ = write!(
1178 buf,
1179 "Usage: {}",
1180 colorize(env.name, [C_BOLD, C_BRIGHT_WHITE].concat().as_str(), env)
1181 );
1182 if let Some(name) = cmd.name {
1183 let _ = write!(buf, " {}", colorize(name, C_MAGENTA, env));
1184 }
1185 if !cmd.subs.is_empty() {
1186 let _ = write!(buf, " {}", colorize("<command>", C_MAGENTA, env));
1187 }
1188 if !cmd.opts.is_empty() {
1189 let _ = write!(buf, " {}", colorize("[options]", C_CYAN, env));
1190 }
1191 for p in &cmd.pos {
1192 if p.min == 0 {
1193 let _ = write!(buf, " [{}]", colorize(p.name, C_YELLOW, env));
1194 } else if p.min == 1 && p.max == 1 {
1195 let _ = write!(buf, " {}", colorize(p.name, C_YELLOW, env));
1196 } else if p.max > 1 {
1197 let _ = write!(buf, " {}...", colorize(p.name, C_YELLOW, env));
1198 }
1199 }
1200 let _ = writeln!(buf);
1201 if let Some(desc) = cmd.desc {
1202 let _ = writeln!(buf, "\n{desc}");
1203 }
1204 if env.auto_help || env.version.is_some() || env.author.is_some() || !cmd.opts.is_empty() {
1205 print_header(&mut buf, "Options", env);
1206 let mut width = 0usize;
1207 if env.auto_help {
1208 width = width.max("-h, --help".len());
1209 }
1210 if env.version.is_some() {
1211 width = width.max("-V, --version".len());
1212 }
1213 if env.author.is_some() {
1214 width = width.max("-A, --author".len());
1215 }
1216 for o in &cmd.opts {
1217 width = width.max(plain_opt_label_len(o));
1218 }
1219
1220 if env.auto_help {
1221 write_row(&mut buf, env, C_CYAN, "-h, --help", "Show this help and exit", width);
1222 }
1223 if env.version.is_some() {
1224 write_row(&mut buf, env, C_CYAN, "-V, --version", "Show version and exit", width);
1225 }
1226 if env.author.is_some() {
1227 write_row(&mut buf, env, C_CYAN, "--author", "Show author and exit", width);
1228 }
1229 for o in &cmd.opts {
1230 let label = make_opt_label(o);
1231 let help = help_text_for_opt(o);
1232 write_row(&mut buf, env, C_CYAN, &label, &help, width);
1233 }
1234 }
1235 if !cmd.subs.is_empty() {
1237 print_header(&mut buf, "Commands", env);
1238 let width = cmd.subs.iter().map(|s| s.name.unwrap_or("<root>").len()).max().unwrap_or(0);
1239 for s in &cmd.subs {
1240 let name = s.name.unwrap_or("<root>");
1241 write_row(&mut buf, env, C_MAGENTA, name, s.desc.unwrap_or(""), width);
1242 }
1243 }
1244 if !cmd.pos.is_empty() {
1246 print_header(&mut buf, "Positionals", env);
1247 let width = cmd.pos.iter().map(|p| p.name.len()).max().unwrap_or(0);
1248 for p in &cmd.pos {
1249 let help = help_for_pos(p);
1250 write_row(&mut buf, env, C_YELLOW, p.name, &help, width);
1251 }
1252 }
1253 let _ = out.write_all(buf.as_bytes());
1254}
1255fn help_for_pos(p: &PosSpec) -> String {
1256 if let Some(d) = p.desc {
1257 return d.to_string();
1258 }
1259 if p.min == 0 {
1260 return "(optional)".to_string();
1261 }
1262 if p.min == 1 && p.max == 1 {
1263 return "(required)".to_string();
1264 }
1265 if p.min == 1 {
1266 return "(at least one required)".to_string();
1267 }
1268 format!("min={} max={}", p.min, p.max)
1269}
1270#[cold]
1272#[inline(never)]
1273pub fn print_version_to<W: Write>(env: &Env<'_>, mut out: W) {
1274 if let Some(v) = env.version {
1275 let _ = writeln!(out, "{v}");
1276 }
1277}
1278#[cold]
1280#[inline(never)]
1281pub fn print_author_to<W: Write>(env: &Env<'_>, mut out: W) {
1282 if let Some(a) = env.author {
1283 let _ = writeln!(out, "{a}");
1284 }
1285}
1286#[cold]
1287#[inline(never)]
1288fn unknown_long_error<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, name: &str) -> Error {
1289 let token = {
1290 let mut s = String::with_capacity(2 + name.len());
1291 s.push_str("--");
1292 s.push_str(name);
1293 s
1294 };
1295 let suggestions = suggest_longs(env, cmd, name);
1296 Error::UnknownOption { token, suggestions }
1297}
1298
1299#[cold]
1300#[inline(never)]
1301fn unknown_short_error<Ctx: ?Sized>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, ch: char) -> Error {
1302 let mut token = String::with_capacity(2);
1303 token.push('-');
1304 token.push(ch);
1305 let suggestions = suggest_shorts(env, cmd, ch);
1306 Error::UnknownOption { token, suggestions }
1307}
1308
1309#[cold]
1310#[inline(never)]
1311fn unknown_command_error<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, tok: &str) -> Error {
1312 Error::UnknownCommand { token: tok.to_string(), suggestions: suggest_cmds(cmd, tok) }
1313}