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