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