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