1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3
4use std::env;
9use std::fmt::{self, Write as _};
10use std::io::{self, Write};
11
12pub type Result<T> = std::result::Result<T, Error>;
15
16pub type OptCallback<Ctx> = for<'a> fn(Option<&'a str>, &mut Ctx) -> Result<()>;
18
19pub type RunCallback<Ctx> = fn(&[&str], &mut Ctx) -> Result<()>;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ArgKind {
25 None,
27 Required,
29 Optional,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum GroupMode {
36 None,
38 Xor,
40 ReqOne,
42}
43#[derive(Clone, Copy)]
45pub enum ValueHint {
46 Any,
48 Number,
50}
51#[derive(Clone, Copy)]
53pub struct OptSpec<'a, Ctx: ?Sized> {
54 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>, }
66
67impl<'a, Ctx: ?Sized> OptSpec<'a, Ctx> {
68 pub const fn new(name: &'a str, cb: OptCallback<Ctx>) -> Self {
70 Self {
71 name,
72 short: None,
73 arg: ArgKind::None,
74 metavar: None,
75 help: "",
76 env: None,
77 default: None,
78 group_id: 0,
79 group_mode: GroupMode::None,
80 value_hint: ValueHint::Any,
81 cb,
82 }
83 }
84 #[must_use]
86 pub const fn numeric(mut self) -> Self {
87 self.value_hint = ValueHint::Number;
88 self
89 }
90 #[must_use]
92 pub const fn short(mut self, short: char) -> Self {
93 self.short = Some(short);
94 self
95 }
96 #[must_use]
98 pub const fn metavar(mut self, metavar: &'a str) -> Self {
99 self.metavar = Some(metavar);
100 self
101 }
102 #[must_use]
104 pub const fn help(mut self, help: &'a str) -> Self {
105 self.help = help;
106 self
107 }
108 #[must_use]
110 pub const fn arg(mut self, arg: ArgKind) -> Self {
111 self.arg = arg;
112 self
113 }
114 #[must_use]
116 pub const fn optional(mut self) -> Self {
117 self.arg = ArgKind::Optional;
118 self
119 }
120 #[must_use]
122 pub const fn required(mut self) -> Self {
123 self.arg = ArgKind::Required;
124 self
125 }
126 #[must_use]
128 pub const fn flag(mut self) -> Self {
129 self.arg = ArgKind::None;
130 self
131 }
132 #[must_use]
134 pub const fn env(mut self, env: &'a str) -> Self {
135 self.env = Some(env);
136 self
137 }
138 #[must_use]
140 pub const fn default(mut self, val: &'a str) -> Self {
141 self.default = Some(val);
142 self
143 }
144 #[must_use]
146 pub const fn at_most_one(mut self, group_id: u16) -> Self {
147 self.group_id = group_id;
148 self.group_mode = GroupMode::Xor;
149 self
150 }
151 #[must_use]
153 pub const fn at_least_one(mut self, group_id: u16) -> Self {
154 self.group_id = group_id;
155 self.group_mode = GroupMode::ReqOne;
156 self
157 }
158}
159
160#[derive(Clone, Copy)]
162pub struct PosSpec<'a> {
163 name: &'a str,
164 desc: Option<&'a str>,
165 min: usize,
166 max: usize,
167}
168
169impl<'a> PosSpec<'a> {
170 #[must_use]
172 pub const fn new(name: &'a str) -> Self {
173 Self { name, desc: None, min: 0, max: 0 }
174 }
175 #[must_use]
177 pub const fn desc(mut self, desc: &'a str) -> Self {
178 self.desc = Some(desc);
179 self
180 }
181 #[must_use]
183 pub const fn one(mut self) -> Self {
184 self.min = 1;
185 self.max = 1;
186 self
187 }
188 #[must_use]
190 pub const fn range(mut self, min: usize, max: usize) -> Self {
191 self.min = min;
192 self.max = max;
193 self
194 }
195}
196
197pub struct CmdSpec<'a, Ctx: ?Sized> {
199 name: Option<&'a str>, desc: Option<&'a str>,
201 opts: Box<[OptSpec<'a, Ctx>]>,
202 subs: Box<[CmdSpec<'a, Ctx>]>,
203 pos: Box<[PosSpec<'a>]>,
204 aliases: Box<[&'a str]>,
205 run: Option<RunCallback<Ctx>>, }
207
208impl<'a, Ctx: ?Sized> CmdSpec<'a, Ctx> {
209 #[must_use]
212 pub fn new(name: Option<&'a str>, run: Option<RunCallback<Ctx>>) -> Self {
213 Self {
214 name,
215 desc: None,
216 opts: Vec::new().into_boxed_slice(),
217 subs: Vec::new().into_boxed_slice(),
218 pos: Vec::new().into_boxed_slice(),
219 aliases: Vec::new().into_boxed_slice(),
220 run,
221 }
222 }
223 #[must_use]
225 pub const fn desc(mut self, desc: &'a str) -> Self {
226 self.desc = Some(desc);
227 self
228 }
229 #[must_use]
231 pub fn opts<S>(mut self, s: S) -> Self
232 where
233 S: Into<Vec<OptSpec<'a, Ctx>>>,
234 {
235 self.opts = s.into().into_boxed_slice();
236 self
237 }
238 #[must_use]
240 pub fn pos<S>(mut self, s: S) -> Self
241 where
242 S: Into<Vec<PosSpec<'a>>>,
243 {
244 self.pos = s.into().into_boxed_slice();
245 self
246 }
247 #[must_use]
249 pub fn subs<S>(mut self, s: S) -> Self
250 where
251 S: Into<Vec<Self>>,
252 {
253 self.subs = s.into().into_boxed_slice();
254 self
255 }
256 #[must_use]
258 pub fn aliases<S>(mut self, s: S) -> Self
259 where
260 S: Into<Vec<&'a str>>,
261 {
262 self.aliases = s.into().into_boxed_slice();
263 self
264 }
265}
266
267pub struct Env<'a> {
269 name: &'a str,
270 version: Option<&'a str>,
271 author: Option<&'a str>,
272 auto_help: bool,
273 wrap_cols: usize,
274 color: bool,
275}
276
277impl<'a> Env<'a> {
278 #[must_use]
280 pub const fn new(name: &'a str) -> Self {
281 Self { name, version: None, author: None, auto_help: false, wrap_cols: 0, color: false }
282 }
283 #[must_use]
285 pub const fn version(mut self, version: &'a str) -> Self {
286 self.version = Some(version);
287 self
288 }
289 #[must_use]
291 pub const fn author(mut self, author: &'a str) -> Self {
292 self.author = Some(author);
293 self
294 }
295 #[must_use]
297 pub const fn auto_help(mut self, auto_help: bool) -> Self {
298 self.auto_help = auto_help;
299 self
300 }
301 #[must_use]
303 pub const fn wrap_cols(mut self, wrap_cols: usize) -> Self {
304 self.wrap_cols = wrap_cols;
305 self
306 }
307 #[must_use]
309 pub const fn color(mut self, color: bool) -> Self {
310 self.color = color;
311 self
312 }
313 #[must_use]
316 pub fn auto_color(mut self) -> Self {
317 self.color = env::var("NO_COLOR").is_err();
318 self
319 }
320}
321
322pub fn dispatch_to<Ctx: ?Sized, W: Write>(
327 env: &Env<'_>,
328 root: &CmdSpec<'_, Ctx>,
329 argv: &[&str],
330 context: &mut Ctx,
331 out: &mut W,
332) -> Result<()> {
333 let mut idx = 0usize;
334 let mut cmd = root;
336 while idx < argv.len() {
337 if let Some(next) = find_sub(cmd, argv[idx]) {
338 cmd = next;
339 idx += 1;
340 } else {
341 break;
342 }
343 }
344 if !cmd.subs.is_empty() && cmd.pos.is_empty() && idx < argv.len() {
347 let tok = argv[idx];
348 if !tok.starts_with('-') && tok != "--" && find_sub(cmd, tok).is_none() {
349 return Err(Error::UnknownCommand(tok.to_string()));
350 }
351 }
352 let mut gcounts: Vec<u8> = vec![0; cmd.opts.len()];
354 let mut pos: Vec<&str> = Vec::with_capacity(argv.len().saturating_sub(idx));
356 let mut stop_opts = false;
357 while idx < argv.len() {
358 let tok = argv[idx];
359 if !stop_opts {
360 if tok == "--" {
361 stop_opts = true;
362 idx += 1;
363 continue;
364 }
365 if tok.starts_with("--") {
366 idx += 1;
367 parse_long(env, cmd, tok, &mut idx, argv, context, &mut gcounts, out)?;
368 continue;
369 }
370 if is_short_like(tok) {
371 idx += 1;
372 parse_short_cluster(env, cmd, tok, &mut idx, argv, context, &mut gcounts, out)?;
373 continue;
374 }
375 }
376 pos.push(tok);
377 idx += 1;
378 }
379 if cmd.pos.is_empty() && !pos.is_empty() {
381 return Err(Error::UnexpectedArgument(pos[0].to_string()));
382 }
383 apply_env_and_defaults(cmd, context, &mut gcounts)?;
384 check_groups(cmd, &gcounts)?;
386 validate_positionals(cmd, &pos)?;
388 if let Some(run) = cmd.run {
390 return run(&pos, context);
391 }
392 Ok(())
393}
394
395pub fn dispatch<Ctx>(
399 env: &Env<'_>,
400 root: &CmdSpec<'_, Ctx>,
401 argv: &[&str],
402 context: &mut Ctx,
403) -> Result<()> {
404 let mut out = io::stdout();
405 dispatch_to(env, root, argv, context, &mut out)
406}
407
408#[non_exhaustive]
410#[derive(Debug, Clone)]
411pub enum Error {
413 UnknownOption(String),
415 MissingValue(String),
417 UnexpectedArgument(String),
419 UnknownCommand(String),
421 GroupViolation(String),
423 MissingPositional(String),
425 TooManyPositional(String),
427 Exit(i32),
429 User(&'static str),
431}
432impl fmt::Display for Error {
433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434 match self {
435 Self::UnknownOption(s) => write!(f, "unknown option: '{s}'"),
436 Self::MissingValue(n) => write!(f, "missing value for --{n}"),
437 Self::UnexpectedArgument(s) => write!(f, "unexpected argument: {s}"),
438 Self::UnknownCommand(s) => write!(f, "unknown command: {s}"),
439 Self::GroupViolation(s) => write!(f, "{s}"),
440 Self::MissingPositional(n) => write!(f, "missing positional: {n}"),
441 Self::TooManyPositional(n) => write!(f, "too many values for: {n}"),
442 Self::Exit(code) => write!(f, "exit {code}"),
443 Self::User(s) => write!(f, "{s}"),
444 }
445 }
446}
447impl std::error::Error for Error {}
448
449fn find_sub<'a, Ctx: ?Sized>(cmd: &'a CmdSpec<'a, Ctx>, name: &str) -> Option<&'a CmdSpec<'a, Ctx>> {
451 for c in &cmd.subs {
452 if let Some(n) = c.name {
453 if n == name {
454 return Some(c);
455 }
456 }
457 if c.aliases.contains(&name) {
458 return Some(c);
459 }
460 }
461 None
462}
463fn apply_env_and_defaults<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, context: &mut Ctx, counts: &mut [u8]) -> Result<()> {
464 if cmd.opts.is_empty() {
465 return Ok(());
466 }
467 if !any_env_or_default(cmd) {
468 return Ok(());
469 }
470
471 for (i, o) in cmd.opts.iter().enumerate() {
473 if let Some(key) = o.env {
474 if let Ok(val) = std::env::var(key) {
475 counts[i] = counts[i].saturating_add(1);
476 (o.cb)(Some(val.as_str()), context)?;
477 }
478 }
479 }
480 for (i, o) in cmd.opts.iter().enumerate() {
482 if counts[i] != 0 {
483 continue;
484 }
485 let Some(def) = o.default else { continue };
486 if o.group_id != 0 {
487 let gid = o.group_id;
488 let mut taken = false;
489 for (j, p) in cmd.opts.iter().enumerate() {
490 if p.group_id == gid && counts[j] != 0 {
491 taken = true;
492 break;
493 }
494 }
495 if taken {
496 continue;
497 }
498 }
499 counts[i] = counts[i].saturating_add(1);
500 (o.cb)(Some(def), context)?;
501 }
502 Ok(())
503}
504
505#[allow(clippy::too_many_arguments)]
506fn parse_long<Ctx: ?Sized, W: std::io::Write>(
507 env: &Env<'_>,
508 cmd: &CmdSpec<'_, Ctx>,
509 tok: &str,
510 idx: &mut usize,
511 argv: &[&str],
512 context: &mut Ctx,
513 counts: &mut [u8],
514 out: &mut W,
515) -> Result<()> {
516 let s = &tok[2..];
518 let (name, attached) = s
519 .as_bytes()
520 .iter()
521 .position(|&b| b == b'=')
522 .map_or((s, None), |eq| (&s[..eq], Some(&s[eq + 1..])));
523 if env.auto_help && name == "help" {
525 print_help_to(env, cmd, out);
526 return Err(Error::Exit(0));
527 }
528 if env.version.is_some() && name == "version" {
529 print_version_to(env, out);
530 return Err(Error::Exit(0));
531 }
532 if env.author.is_some() && name == "author" {
533 print_author_to(env, out);
534 return Err(Error::Exit(0));
535 }
536 let (i, spec) = match cmd.opts.iter().enumerate().find(|(_, o)| o.name == name) {
537 Some(x) => x,
538 None => return Err(unknown_long_error(name)),
539 };
540 counts[i] = counts[i].saturating_add(1);
541 match spec.arg {
542 ArgKind::None => {
543 (spec.cb)(None, context)?;
544 }
545 ArgKind::Required => {
546 let v = if let Some(a) = attached {
547 if a.is_empty() {
548 return Err(Error::MissingValue(spec.name.to_string()));
549 }
550 a
551 } else {
552 take_next(idx, argv).ok_or_else(|| Error::MissingValue(spec.name.to_string()))?
553 };
554 (spec.cb)(Some(v), context)?;
555 }
556 ArgKind::Optional => {
557 let v = match (attached, argv.get(*idx).copied()) {
558 (Some(a), _) => Some(a),
559 (None, Some("-")) => {
560 *idx += 1; None
562 }
563 (None, Some(n))
564 if matches!(spec.value_hint, ValueHint::Number) && is_dash_number(n) =>
565 {
566 *idx += 1;
567 Some(n)
568 }
569 (None, Some(n)) if looks_value_like(n) => {
570 *idx += 1;
571 Some(n)
572 }
573 _ => None,
574 };
575 (spec.cb)(v, context)?;
576 }
577 }
578 Ok(())
579}
580
581#[allow(clippy::too_many_arguments)]
582fn parse_short_cluster<Ctx: ?Sized, W: std::io::Write>(
583 env: &Env<'_>,
584 cmd: &CmdSpec<'_, Ctx>,
585 tok: &str,
586 idx: &mut usize,
587 argv: &[&str],
588 context: &mut Ctx,
589 counts: &mut [u8],
590 out: &mut W,
591) -> Result<()> {
592 let short_idx = build_short_idx(cmd);
594 let s = &tok[1..];
595 let bytes = s.as_bytes();
596 let mut i = 0usize;
597 while i < bytes.len() {
598 let (ch, adv) = if bytes[i] < 128 {
600 (bytes[i] as char, 1)
601 } else {
602 let c = s[i..].chars().next().unwrap();
603 (c, c.len_utf8())
604 };
605 i += adv;
606
607 if env.auto_help && ch == 'h' {
609 print_help_to(env, cmd, out);
610 return Err(Error::Exit(0));
611 }
612 if env.version.is_some() && ch == 'V' {
613 print_version_to(env, out);
614 return Err(Error::Exit(0));
615 }
616 if env.author.is_some() && ch == 'A' {
617 print_author_to(env, out);
618 return Err(Error::Exit(0));
619 }
620 let (oi, spec) = match lookup_short(cmd, &short_idx, ch) {
621 Some(x) => x,
622 None => return Err(unknown_short_error(ch)),
623 };
624 counts[oi] = counts[oi].saturating_add(1);
625 match spec.arg {
626 ArgKind::None => {
627 (spec.cb)(None, context)?;
628 }
629 ArgKind::Required => {
630 if i < s.len() {
631 let rem = &s[i..];
632 (spec.cb)(Some(rem), context)?;
633 return Ok(());
634 }
635 let v = take_next(idx, argv)
636 .ok_or_else(|| Error::MissingValue(spec.name.to_string()))?;
637 (spec.cb)(Some(v), context)?;
638 return Ok(());
639 }
640 ArgKind::Optional => {
641 if i < s.len() {
642 let rem = &s[i..];
643 (spec.cb)(Some(rem), context)?;
644 return Ok(());
645 }
646 let v = match argv.get(*idx) {
648 Some(&"-") => {
649 *idx += 1;
650 None
651 }
652 Some(n)
654 if matches!(spec.value_hint, ValueHint::Number) && is_dash_number(n) =>
655 {
656 *idx += 1;
657 Some(n)
658 }
659 Some(n) if looks_value_like(n) => {
661 *idx += 1;
662 Some(n)
663 }
664 _ => None,
665 };
666 (spec.cb)(v.map(|v| &**v), context)?;
667 return Ok(());
668 }
669 }
670 }
671 Ok(())
672}
673
674#[inline]
675fn any_env_or_default<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>) -> bool {
676 cmd.opts.iter().any(|o| o.env.is_some() || o.default.is_some())
677}
678#[inline]
679fn take_next<'a>(idx: &mut usize, argv: &'a [&'a str]) -> Option<&'a str> {
680 let i = *idx;
681 if i < argv.len() {
682 *idx = i + 1;
683 Some(argv[i])
684 } else {
685 None
686 }
687}
688#[inline]
689fn is_short_like(s: &str) -> bool {
690 let b = s.as_bytes();
691 b.len() >= 2 && b[0] == b'-' && b[1] != b'-'
692}
693#[inline]
694fn is_dash_number(s: &str) -> bool {
695 let b = s.as_bytes();
696 if b.is_empty() || b[0] != b'-' {
697 return false;
698 }
699 if b.len() == 1 {
701 return false;
702 }
703 is_numeric_like(&b[1..])
704}
705#[inline]
706fn looks_value_like(s: &str) -> bool {
707 if !s.starts_with('-') {
708 return true;
709 }
710 if s == "-" {
711 return false;
712 }
713 is_numeric_like(&s.as_bytes()[1..])
714}
715#[inline]
716fn is_numeric_like(b: &[u8]) -> bool {
717 let mut i = 0;
719 let n = b.len();
720 if i < n && b[i] == b'.' {
722 i += 1;
723 }
724 let mut nd = 0;
726 while i < n && (b[i] as char).is_ascii_digit() {
727 i += 1;
728 nd += 1;
729 }
730 if nd == 0 {
731 return false;
732 }
733 if i < n && b[i] == b'.' {
735 i += 1;
736 while i < n && (b[i] as char).is_ascii_digit() {
737 i += 1;
738 }
739 }
740 if i < n && (b[i] == b'e' || b[i] == b'E') {
742 i += 1;
743 if i < n && (b[i] == b'+' || b[i] == b'-') {
744 i += 1;
745 }
746 let mut ed = 0;
747 while i < n && (b[i] as char).is_ascii_digit() {
748 i += 1;
749 ed += 1;
750 }
751 if ed == 0 {
752 return false;
753 }
754 }
755 i == n
756}
757
758fn check_groups<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, counts: &[u8]) -> Result<()> {
759 let opts = &cmd.opts;
760 let opts_len = opts.len();
761 let mut index = 0usize;
762 while index < opts_len {
763 let id = opts[index].group_id;
764 if id != 0 {
765 let mut seen = false;
767 let mut k = 0usize;
768 while k < index {
769 if opts[k].group_id == id {
770 seen = true;
771 break;
772 }
773 k += 1;
774 }
775 if !seen {
776 let mut total = 0u32;
777 let mut xor = false;
778 let mut req = false;
779 let mut j = 0usize;
780 while j < opts_len {
781 let o = &opts[j];
782 if o.group_id == id {
783 total += u32::from(counts[j]);
784 match o.group_mode {
785 GroupMode::Xor => xor = true,
786 GroupMode::ReqOne => req = true,
787 GroupMode::None => {}
788 }
789 if xor && total > 1 {
790 return Err(Error::GroupViolation(group_msg(opts, id, true)));
791 }
792 }
793 j += 1;
794 }
795 if req && total == 0 {
796 return Err(Error::GroupViolation(group_msg(opts, id, false)));
797 }
798 }
799 }
800 index += 1;
801 }
802 Ok(())
803}
804
805#[cold]
806#[inline(never)]
807fn group_msg<Ctx: ?Sized>(opts: &[OptSpec<'_, Ctx>], id: u16, xor: bool) -> String {
808 let mut names = String::new();
809 for o in opts.iter().filter(|o| o.group_id == id) {
810 if !names.is_empty() {
811 names.push_str(" | ");
812 }
813 names.push_str(o.name);
814 }
815 if xor {
816 format!("at most one of the following options may be used: {names}")
817 } else {
818 format!("one of the following options is required: {names}")
819 }
820}
821
822fn validate_positionals<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>, pos: &[&str]) -> Result<()> {
823 if cmd.pos.is_empty() {
824 return Ok(());
825 }
826 let total = pos.len();
827 let mut min_sum: usize = 0;
828 let mut max_sum: Option<usize> = Some(0);
829 for p in &cmd.pos {
830 min_sum = min_sum.saturating_add(p.min);
831 if let Some(ms) = max_sum {
832 if p.max == usize::MAX {
833 max_sum = None;
834 } else {
835 max_sum = Some(ms.saturating_add(p.max));
836 }
837 }
838 }
839 if total < min_sum {
841 let mut need = 0usize;
842 for p in &cmd.pos {
843 need = need.saturating_add(p.min);
844 if total < need {
845 return Err(Error::MissingPositional(p.name.to_string()));
846 }
847 }
848 return Err(Error::MissingPositional(
850 cmd.pos.first().map_or("<args>", |p| p.name).to_string(),
851 ));
852 }
853 if let Some(ms) = max_sum {
855 if total > ms {
856 let last = cmd.pos.last().map_or("<args>", |p| p.name);
857 return Err(Error::TooManyPositional(last.to_string()));
858 }
859 }
860 Ok(())
861}
862#[inline]
863const fn plain_opt_label_len<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> usize {
864 let mut len = if o.short.is_some() { 4 } else { 0 }; len += 2 + o.name.len(); if let Some(m) = o.metavar {
867 len += 1 + m.len();
868 }
869 len
870}
871#[inline]
872fn make_opt_label<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> String {
873 let mut s = String::new();
874 if let Some(ch) = o.short {
875 s.push('-');
876 s.push(ch);
877 s.push(',');
878 s.push(' ');
879 }
880 s.push_str("--");
881 s.push_str(o.name);
882 if let Some(m) = o.metavar {
883 s.push(' ');
884 s.push_str(m);
885 }
886 s
887}
888
889const C_BOLD: &str = "\u{001b}[1m";
891const C_UNDERLINE: &str = "\u{001b}[4m";
892const C_BRIGHT_WHITE: &str = "\u{001b}[97m";
893const C_CYAN: &str = "\u{001b}[36m";
894const C_MAGENTA: &str = "\u{001b}[35m";
895const C_YELLOW: &str = "\u{001b}[33m";
896const C_RESET: &str = "\u{001b}[0m";
897#[inline]
898fn colorize(s: &str, color: &str, env: &Env) -> String {
899 if !env.color || color.is_empty() {
900 s.to_string()
901 } else {
902 format!("{color}{s}{C_RESET}")
903 }
904}
905#[inline]
906fn help_text_for_opt<Ctx: ?Sized>(o: &OptSpec<'_, Ctx>) -> String {
907 match (o.env, o.default) {
908 (Some(k), Some(d)) => format!("{} (env {k}, default={d})", o.help),
909 (Some(k), None) => format!("{} (env {k})", o.help),
910 (None, Some(d)) => format!("{} (default={d})", o.help),
911 (None, None) => o.help.to_string(),
912 }
913}
914#[inline]
915fn print_header(buf: &mut String, text: &str, env: &Env) {
916 let _ = writeln!(buf, "\n{}:", colorize(text, &[C_BOLD, C_UNDERLINE].concat(), env).as_str());
917}
918#[inline]
919fn lookup_short<'a, Ctx: ?Sized>(
920 cmd: &'a CmdSpec<'a, Ctx>,
921 table: &[u16; 128],
922 ch: char,
923) -> Option<(usize, &'a OptSpec<'a, Ctx>)> {
924 let c = ch as u32;
925 if c < 128 {
926 let i = table[c as usize];
927 if i != u16::MAX {
928 let idx = i as usize;
929 return Some((idx, &cmd.opts[idx]));
930 }
931 return None;
932 }
933 cmd.opts.iter().enumerate().find(|(_, o)| o.short == Some(ch))
934}
935fn build_short_idx<Ctx: ?Sized>(cmd: &CmdSpec<'_, Ctx>) -> [u16; 128] {
936 let mut map = [u16::MAX; 128];
937 let mut i = 0usize;
938 let len = cmd.opts.len();
939 while i < len {
940 let o = &cmd.opts[i];
941 if let Some(ch) = o.short {
942 let cu = ch as usize;
943 if cu < 128 {
944 debug_assert!(u16::try_from(i).is_ok());
945 map[cu] = u16::try_from(i).unwrap_or(0); }
947 }
948 i += 1;
949 }
950 map
951}
952#[inline]
953fn write_wrapped(buf: &mut String, text: &str, indent_cols: usize, wrap_cols: usize) {
954 if wrap_cols == 0 {
955 let _ = writeln!(buf, "{text}");
956 return;
957 }
958 let mut col = indent_cols;
959 let mut first = true;
960 for word in text.split_whitespace() {
961 let wlen = word.len();
962 if first {
963 for _ in 0..indent_cols {
964 buf.push(' ');
965 }
966 buf.push_str(word);
967 col = indent_cols + wlen;
968 first = false;
969 continue;
970 }
971 if col + 1 + wlen > wrap_cols {
972 buf.push('\n');
973 for _ in 0..indent_cols {
974 buf.push(' ');
975 }
976 buf.push_str(word);
977 col = indent_cols + wlen;
978 } else {
979 buf.push(' ');
980 buf.push_str(word);
981 col += 1 + wlen;
982 }
983 }
984 buf.push('\n');
985}
986
987fn write_row(
988 buf: &mut String,
989 env: &Env,
990 color: &str,
991 plain_label: &str,
992 help: &str,
993 label_col: usize,
994) {
995 let _ = write!(buf, " {}", colorize(plain_label, color, env));
996 let pad = label_col.saturating_sub(plain_label.len());
997 for _ in 0..pad {
998 buf.push(' ');
999 }
1000 buf.push(' ');
1001 buf.push(' ');
1002 let indent = 4 + label_col;
1003 write_wrapped(buf, help, indent, env.wrap_cols);
1004}
1005
1006#[cold]
1008#[inline(never)]
1009pub fn print_help_to<Ctx: ?Sized, W: Write>(env: &Env<'_>, cmd: &CmdSpec<'_, Ctx>, mut out: W) {
1010 let mut buf = String::new();
1011 let _ = write!(
1012 buf,
1013 "Usage: {}",
1014 colorize(env.name, [C_BOLD, C_BRIGHT_WHITE].concat().as_str(), env)
1015 );
1016 if let Some(name) = cmd.name {
1017 let _ = write!(buf, " {}", colorize(name, C_MAGENTA, env));
1018 }
1019 if !cmd.subs.is_empty() {
1020 let _ = write!(buf, " {}", colorize("<command>", C_MAGENTA, env));
1021 }
1022 if !cmd.opts.is_empty() {
1023 let _ = write!(buf, " {}", colorize("[options]", C_CYAN, env));
1024 }
1025 for p in &cmd.pos {
1026 if p.min == 0 {
1027 let _ = write!(buf, " [{}]", colorize(p.name, C_YELLOW, env));
1028 } else if p.min == 1 && p.max == 1 {
1029 let _ = write!(buf, " {}", colorize(p.name, C_YELLOW, env));
1030 } else if p.max > 1 {
1031 let _ = write!(buf, " {}...", colorize(p.name, C_YELLOW, env));
1032 }
1033 }
1034 let _ = writeln!(buf);
1035 if let Some(desc) = cmd.desc {
1036 let _ = writeln!(buf, "\n{desc}");
1037 }
1038 if env.auto_help || env.version.is_some() || env.author.is_some() || !cmd.opts.is_empty() {
1039 print_header(&mut buf, "Options", env);
1040 let mut width = 0usize;
1041 if env.auto_help {
1042 width = width.max("-h, --help".len());
1043 }
1044 if env.version.is_some() {
1045 width = width.max("-V, --version".len());
1046 }
1047 if env.author.is_some() {
1048 width = width.max("-A, --author".len());
1049 }
1050 for o in &cmd.opts {
1051 width = width.max(plain_opt_label_len(o));
1052 }
1053
1054 if env.auto_help {
1055 write_row(&mut buf, env, C_CYAN, "-h, --help", "Show this help and exit", width);
1056 }
1057 if env.version.is_some() {
1058 write_row(&mut buf, env, C_CYAN, "-V, --version", "Show version and exit", width);
1059 }
1060 if env.author.is_some() {
1061 write_row(&mut buf, env, C_CYAN, "--author", "Show author and exit", width);
1062 }
1063 for o in &cmd.opts {
1064 let label = make_opt_label(o);
1065 let help = help_text_for_opt(o);
1066 write_row(&mut buf, env, C_CYAN, &label, &help, width);
1067 }
1068 }
1069 if !cmd.subs.is_empty() {
1071 print_header(&mut buf, "Commands", env);
1072 let width = cmd.subs.iter().map(|s| s.name.unwrap_or("<root>").len()).max().unwrap_or(0);
1073 for s in &cmd.subs {
1074 let name = s.name.unwrap_or("<root>");
1075 write_row(&mut buf, env, C_MAGENTA, name, s.desc.unwrap_or(""), width);
1076 }
1077 }
1078 if !cmd.pos.is_empty() {
1080 print_header(&mut buf, "Positionals", env);
1081 let width = cmd.pos.iter().map(|p| p.name.len()).max().unwrap_or(0);
1082 for p in &cmd.pos {
1083 let help = help_for_pos(p);
1084 write_row(&mut buf, env, C_YELLOW, p.name, &help, width);
1085 }
1086 }
1087 let _ = out.write_all(buf.as_bytes());
1088}
1089fn help_for_pos(p: &PosSpec) -> String {
1090 if let Some(d) = p.desc {
1091 return d.to_string();
1092 }
1093 if p.min == 0 {
1094 return "(optional)".to_string();
1095 }
1096 if p.min == 1 && p.max == 1 {
1097 return "(required)".to_string();
1098 }
1099 if p.min == 1 {
1100 return "(at least one required)".to_string();
1101 }
1102 format!("min={} max={}", p.min, p.max)
1103}
1104#[cold]
1106#[inline(never)]
1107pub fn print_version_to<W: Write>(env: &Env<'_>, mut out: W) {
1108 if let Some(v) = env.version {
1109 let _ = writeln!(out, "{v}");
1110 }
1111}
1112#[cold]
1114#[inline(never)]
1115pub fn print_author_to<W: Write>(env: &Env<'_>, mut out: W) {
1116 if let Some(a) = env.author {
1117 let _ = writeln!(out, "{a}");
1118 }
1119}
1120#[cold]
1121#[inline(never)]
1122fn unknown_long_error(name: &str) -> Error {
1123 Error::UnknownOption({
1124 let mut s = String::with_capacity(2 + name.len());
1125 s.push_str("--");
1126 s.push_str(name);
1127 s
1128 })
1129}
1130
1131#[cold]
1132#[inline(never)]
1133fn unknown_short_error(ch: char) -> Error {
1134 Error::UnknownOption({
1135 let mut s = String::with_capacity(2);
1136 s.push('-');
1137 s.push(ch);
1138 s
1139 })
1140}