1use crate::{Spec, SpecCommand};
2use std::sync::LazyLock;
3use tera::Tera;
4
5mod style;
6pub use style::Style;
7
8pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String {
9 render_help_styled(spec, cmd, long, Style::PLAIN)
10}
11
12pub fn render_help_styled(spec: &Spec, cmd: &SpecCommand, long: bool, style: Style) -> String {
14 let docs_spec = crate::docs::models::Spec::from(spec);
16 let mut docs_cmd = crate::docs::models::SpecCommand::from(&without_hidden(cmd, long));
17
18 let mut ctx = tera::Context::new();
19 ctx.insert("spec", &docs_spec);
20 ctx.insert("long", &long);
21 ctx.insert("root", &docs_cmd.full_cmd.is_empty());
25 ctx.insert("show_help_subcommand", &!cmd.disable_help_subcommand);
29 let (mut inherited, ancestors_taken) = inherited_flags(spec, cmd, &docs_cmd.full_cmd, long);
36
37 {
45 let supplied = supplied_flags(spec, cmd, &ancestors_taken, docs_cmd.full_cmd.is_empty());
46 if !supplied.is_empty() {
47 match docs_cmd
48 .flag_groups
49 .iter_mut()
50 .find(|g| g.heading.is_none())
51 {
52 Some(group) => group.items.extend(supplied),
53 None => docs_cmd.flag_groups.insert(
58 0,
59 crate::docs::models::Group {
60 heading: None,
61 help: None,
62 help_rendered: None,
63 items: supplied,
64 },
65 ),
66 }
67 }
68 }
69
70 let width = crate::docs::layout::help_width(cmd.term_width, cmd.max_term_width);
71 ctx.insert("terminal_width", &width);
72 lay_out_group_help(&mut docs_cmd.subcommand_groups, width);
73 lay_out_group_help(&mut docs_cmd.arg_groups, width);
74 lay_out_group_help(&mut docs_cmd.flag_groups, width);
75 let col = crate::docs::layout::usage_column_width(
76 docs_cmd
77 .flag_groups
78 .iter()
79 .flat_map(|g| g.items.iter())
80 .chain(inherited.iter())
81 .map(|f| f.display_usage.as_str()),
82 width,
83 );
84 let flag_column = Column {
85 width,
86 col,
87 long,
88 next_line: cmd.next_line_help,
89 };
90 for group in &mut docs_cmd.flag_groups {
91 lay_out(&mut group.items, flag_column);
92 }
93 lay_out(&mut inherited, flag_column);
94
95 let arg_col = crate::docs::layout::usage_column_width(
99 docs_cmd.args.iter().map(|a| a.usage.as_str()),
100 width,
101 );
102 for group in &mut docs_cmd.arg_groups {
103 lay_out_args(
104 &mut group.items,
105 Column {
106 width,
107 col: arg_col,
108 long,
109 next_line: cmd.next_line_help,
110 },
111 );
112 }
113
114 lay_out_flattened(&mut docs_cmd.flattened_subcommands, width, long);
117
118 let help_row = {
123 let show_help_row = !docs_cmd.subcommands.is_empty()
124 && !docs_cmd.flatten_help
125 && !cmd.disable_help_subcommand;
126 let cmd_col = crate::docs::layout::usage_column_width(
127 docs_cmd
128 .subcommand_groups
129 .iter()
130 .flat_map(|g| g.items.iter())
131 .map(|c| c.name.as_str())
132 .chain(show_help_row.then_some(HELP_SUBCOMMAND)),
133 width,
134 );
135 for group in &mut docs_cmd.subcommand_groups {
136 lay_out_commands(&mut group.items, width, cmd_col, cmd.next_line_help);
137 }
138 show_help_row.then(|| {
142 render_row(
143 HELP_SUBCOMMAND,
144 HELP_SUBCOMMAND_SUMMARY,
145 width,
146 cmd_col,
147 cmd.next_line_help,
148 )
149 })
150 };
151 ctx.insert("help_row", &help_row);
152
153 let arg_has_ungrouped = docs_cmd
154 .arg_groups
155 .iter()
156 .any(|group| group.heading.is_none());
157 let arg_has_grouped = docs_cmd
158 .arg_groups
159 .iter()
160 .any(|group| group.heading.is_some());
161 let flag_has_ungrouped = docs_cmd
162 .flag_groups
163 .iter()
164 .any(|group| group.heading.is_none());
165 let flag_has_grouped = docs_cmd
166 .flag_groups
167 .iter()
168 .any(|group| group.heading.is_some());
169 ctx.insert("arg_has_ungrouped", &arg_has_ungrouped);
170 ctx.insert("arg_has_grouped", &arg_has_grouped);
171 ctx.insert("flag_has_ungrouped", &flag_has_ungrouped);
172 ctx.insert("flag_has_grouped", &flag_has_grouped);
173
174 ctx.insert("cmd", &docs_cmd);
177 ctx.insert("global_flags", &inherited);
178 for (name, mark) in MARKS {
179 ctx.insert(name, &mark);
180 }
181 ctx.insert("mark_grouped_args", &MARK_GROUPED_ARGS);
182 ctx.insert("mark_grouped_flags", &MARK_GROUPED_FLAGS);
183 ctx.insert("mark_global_flags", &MARK_GLOBAL_FLAGS);
184 let template = if long {
185 "spec_template_long.tera"
186 } else {
187 "spec_template_short.tera"
188 };
189 let rendered = TERA.render(template, &ctx).unwrap();
190 let sections = Sections::split(&rendered);
191 let styling = style::Styling::new(&docs_cmd, &inherited, sections.usage);
192 let page = match spec
193 .help_template
194 .as_deref()
195 .filter(|t| crate::help_template::is_set(t))
196 {
197 Some(template) => {
198 crate::help_template::substitute_with_style(template, style.coloured, |name| {
199 sections
200 .named(name)
201 .map(|section| styling.apply(§ion, style))
202 })
203 }
204 None => styling.apply(§ions.concatenated(), style),
205 };
206 let page = if style.coloured {
207 std::borrow::Cow::Borrowed(page.as_str())
208 } else {
209 crate::docs::strip_ansi(&page)
210 };
211 page.trim().to_string() + "\n"
212}
213
214const MARKS: [(&str, &str); 6] = [
225 ("mark_usage", "\u{1}usage\u{1}"),
226 ("mark_commands", "\u{1}commands\u{1}"),
227 ("mark_args", "\u{1}args\u{1}"),
228 ("mark_flags", "\u{1}flags\u{1}"),
229 ("mark_flattened", "\u{1}flattened\u{1}"),
230 ("mark_after_help", "\u{1}after_help\u{1}"),
231];
232const MARK_GROUPED_ARGS: &str = "\u{1}grouped_args\u{1}";
233const MARK_GROUPED_FLAGS: &str = "\u{1}grouped_flags\u{1}";
234const MARK_GLOBAL_FLAGS: &str = "\u{1}global_flags\u{1}";
235
236struct Sections<'a> {
242 about: &'a str,
243 usage: &'a str,
244 commands: &'a str,
245 args: String,
246 flags: String,
247 grouped_args: &'a str,
248 ungrouped_args: &'a str,
249 grouped_flags: &'a str,
250 ungrouped_flags: String,
251 flattened: &'a str,
252 after_help: &'a str,
253}
254
255impl<'a> Sections<'a> {
256 fn split(rendered: &'a str) -> Self {
257 let mut rest = rendered;
258 let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
259 for (_, mark) in MARKS {
260 match rest.split_once(mark) {
264 Some((before, after)) => {
265 parts.push(before);
266 rest = after;
267 }
268 None => parts.push(""),
269 }
270 }
271 parts.push(rest);
272 let (ungrouped_args, grouped_args) = parts[3]
273 .split_once(MARK_GROUPED_ARGS)
274 .unwrap_or((parts[3], ""));
275 let (own_flags, global_flags) = parts[4]
276 .split_once(MARK_GLOBAL_FLAGS)
277 .unwrap_or((parts[4], ""));
278 let (own_ungrouped_flags, grouped_flags) = own_flags
279 .split_once(MARK_GROUPED_FLAGS)
280 .unwrap_or((own_flags, ""));
281 Self {
282 about: parts[0],
283 usage: parts[1],
284 commands: parts[2],
285 args: format!("{ungrouped_args}{grouped_args}"),
286 flags: format!("{own_ungrouped_flags}{grouped_flags}{global_flags}"),
287 grouped_args,
288 ungrouped_args,
289 grouped_flags,
290 ungrouped_flags: format!("{own_ungrouped_flags}{global_flags}"),
291 flattened: parts[5],
292 after_help: parts[6],
293 }
294 }
295
296 fn concatenated(&self) -> String {
298 [
299 self.about,
300 self.usage,
301 self.commands,
302 self.args.as_str(),
303 self.flags.as_str(),
304 self.flattened,
305 self.after_help,
306 ]
307 .concat()
308 }
309
310 fn named(&self, name: &str) -> Option<String> {
312 Some(match name {
313 "about" => self.about.trim().to_string(),
314 "usage" => self.usage.trim().to_string(),
315 "commands" => {
316 let mut out = self.commands.trim().to_string();
317 let flattened = self.flattened.trim();
318 if !flattened.is_empty() {
319 if !out.is_empty() {
320 out.push_str("\n\n");
321 }
322 out.push_str(flattened);
323 }
324 out
325 }
326 "args" => self.args.trim().to_string(),
327 "flags" => self.flags.trim().to_string(),
328 "grouped_args" => self.grouped_args.trim().to_string(),
329 "ungrouped_args" => self.ungrouped_args.trim().to_string(),
330 "grouped_flags" => self.grouped_flags.trim().to_string(),
331 "ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
332 "after_help" => self.after_help.trim().to_string(),
333 _ => return None,
334 })
335 }
336}
337
338fn supplied_flags(
352 spec: &Spec,
353 cmd: &SpecCommand,
354 ancestors_taken: &[String],
355 is_root: bool,
356) -> Vec<crate::docs::models::SpecFlag> {
357 let mut taken: Vec<String> = ancestors_taken.to_vec();
362 for f in &cmd.flags {
363 taken.extend(f.long.iter().map(|l| format!("--{l}")));
364 taken.extend(f.short.iter().map(|s| format!("-{s}")));
365 taken.extend(f.negate.clone());
367 }
368
369 let build = |name: &str, long: &str, short: char, help: &str| {
370 let long_free = !taken.contains(&format!("--{long}"));
371 let short_free = !taken.contains(&format!("-{short}"));
372 if !long_free && !short_free {
373 return None;
374 }
375 let name = if long_free { name } else { &short.to_string() };
378 let mut flag = crate::SpecFlag {
379 name: name.to_string(),
380 long: if long_free {
381 vec![long.to_string()]
382 } else {
383 vec![]
384 },
385 short: if short_free { vec![short] } else { vec![] },
386 help: Some(help.to_string()),
387 ..Default::default()
388 };
389 flag.usage = flag.usage();
390 Some(crate::docs::models::SpecFlag::from(&flag))
391 };
392
393 let mut out = Vec::new();
394 if spec.disable_help != Some(true) && !cmd.disable_help_flag {
401 out.extend(build("help", "help", 'h', "Print help"));
402 }
403 if is_root
404 && (spec.version.is_some() || spec.long_version.is_some())
405 && !cmd.disable_version_flag
406 {
407 out.extend(build("version", "version", 'V', "Print version"));
408 }
409 out
410}
411
412fn lay_out(flags: &mut [crate::docs::models::SpecFlag], column: Column) {
419 for flag in flags {
420 flag.usage_col_width = column.col;
421 flag.help_is_block =
422 !column.can_inline(crate::docs::layout::visible_width(&flag.display_usage));
423 let text = if column.long {
424 flag.help_long
425 .as_deref()
426 .or(flag.help.as_deref())
427 .map(str::to_string)
428 } else if column.next_line {
429 flag.help.clone()
430 } else {
431 with_annotations(flag.help.as_deref(), flag_annotations(flag))
432 };
433 wrap_into(
434 text,
435 column,
436 crate::docs::layout::visible_width(&flag.display_usage),
437 &mut flag.row,
438 &mut flag.help_rendered,
439 &mut flag.help_is_multiline,
440 &mut flag.ann_indent,
441 );
442 }
443}
444
445fn lay_out_args(args: &mut [crate::docs::models::SpecArg], column: Column) {
451 for arg in args {
452 arg.usage_col_width = column.col;
453 arg.help_is_block = !column.can_inline(crate::docs::layout::visible_width(&arg.usage));
454 let text = if column.long {
455 arg.help_long
456 .as_deref()
457 .or(arg.help.as_deref())
458 .map(str::to_string)
459 } else if column.next_line {
460 arg.help.clone()
461 } else {
462 with_annotations(arg.help.as_deref(), arg_annotations(arg))
463 };
464 wrap_into(
465 text,
466 column,
467 crate::docs::layout::visible_width(&arg.usage),
468 &mut arg.row,
469 &mut arg.help_rendered,
470 &mut arg.help_is_multiline,
471 &mut arg.ann_indent,
472 );
473 }
474}
475
476fn lay_out_flattened(commands: &mut [crate::docs::models::SpecCommand], width: usize, long: bool) {
477 for command in commands {
478 lay_out_group_help(&mut command.arg_groups, width);
479 lay_out_group_help(&mut command.flag_groups, width);
480 let arg_col = crate::docs::layout::usage_column_width(
481 command
482 .arg_groups
483 .iter()
484 .flat_map(|group| group.items.iter())
485 .map(|arg| arg.usage.as_str()),
486 width,
487 );
488 let arg_column = Column {
489 width,
490 col: arg_col,
491 long,
492 next_line: command.flattened_next_line_help,
493 };
494 for group in &mut command.arg_groups {
495 lay_out_args(&mut group.items, arg_column);
496 }
497
498 let flag_col = crate::docs::layout::usage_column_width(
499 command
500 .flag_groups
501 .iter()
502 .flat_map(|group| group.items.iter())
503 .map(|flag| flag.display_usage.as_str()),
504 width,
505 );
506 let flag_column = Column {
507 width,
508 col: flag_col,
509 long,
510 next_line: command.flattened_next_line_help,
511 };
512 for group in &mut command.flag_groups {
513 lay_out(&mut group.items, flag_column);
514 }
515 }
516}
517
518fn lay_out_group_help<T>(groups: &mut [crate::docs::models::Group<T>], width: usize) {
519 for group in groups {
520 group.help_rendered = group
521 .help
522 .as_deref()
523 .map(|help| crate::docs::layout::render_indented_text(help, width, 2));
524 }
525}
526
527fn wrap_into(
533 text: Option<String>,
534 column: Column,
535 usage_width: usize,
536 row: &mut Option<String>,
537 help_rendered: &mut Option<String>,
538 help_is_multiline: &mut bool,
539 ann_indent: &mut String,
540) {
541 *row = None;
542 *help_rendered = None;
543 *help_is_multiline = false;
544 *ann_indent = column.annotation_indent(column.can_inline(usage_width));
547 let Some(text) = text else { return };
548 if !column.can_inline(usage_width) {
549 let indent = column.block_indent();
550 *row = Some(indent_text(
551 &crate::docs::layout::render_indented_text(&text, column.width, indent),
552 indent,
553 ));
554 return;
555 }
556 let first_indent = column.inline_help_start(usage_width);
557 let continuation_indent = column.description_indent();
558 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
559 &text,
560 column.width,
561 first_indent,
562 continuation_indent,
563 );
564 *ann_indent = column.annotation_indent(!rendered.is_empty());
568 if !rendered.is_empty() {
569 *help_rendered = Some(rendered);
570 *help_is_multiline = is_multiline;
571 }
572 *row = Some(text);
573}
574
575fn indent_text(text: &str, indent: usize) -> String {
576 let pad = " ".repeat(indent);
577 text.lines()
578 .map(|line| {
579 if line.is_empty() {
580 String::new()
581 } else {
582 format!("{pad}{line}")
583 }
584 })
585 .collect::<Vec<_>>()
586 .join("\n")
587}
588
589fn with_annotations(help: Option<&str>, annotations: Vec<String>) -> Option<String> {
596 let mut parts = Vec::new();
597 if let Some(help) = summarize(help) {
598 parts.push(help.to_string());
599 }
600 parts.extend(annotations);
601 (!parts.is_empty()).then(|| parts.join(" "))
602}
603
604fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec<String> {
606 let mut parts = value_annotations(
607 flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()),
608 flag.hide_possible_values,
609 flag.env.as_deref(),
610 flag.hide_env,
611 &flag.env_fallback,
612 &flag.deprecated_env,
613 &flag.default,
614 flag.hide_default_value,
615 );
616 if let Some(label) = deprecation_label(
617 flag.deprecated.as_deref(),
618 flag.deprecated_warn_at.as_deref(),
619 flag.deprecated_remove_at.as_deref(),
620 ) {
621 parts.push(label);
622 }
623 parts
624}
625
626fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec<String> {
628 value_annotations(
629 arg.choices.as_ref(),
630 arg.hide_possible_values,
631 arg.env.as_deref(),
632 arg.hide_env,
633 &arg.env_fallback,
634 &arg.deprecated_env,
635 &arg.default,
636 arg.hide_default_value,
637 )
638}
639
640#[allow(clippy::too_many_arguments)]
642fn value_annotations(
643 choices: Option<&crate::SpecChoices>,
644 hide_possible_values: bool,
645 env: Option<&str>,
646 hide_env: bool,
647 env_fallback: &[String],
648 deprecated_env: &[String],
649 default: &[String],
650 hide_default_value: bool,
651) -> Vec<String> {
652 let mut parts = Vec::new();
653 if let Some(choices) = choices.filter(|_| !hide_possible_values) {
654 if !choices.choices.is_empty() {
655 parts.push(format!("[{}]", choices.choices.join(", ")));
656 }
657 if let Some(env) = choices.env() {
658 parts.push(format!("[choices env: {env}]"));
659 }
660 }
661 if !hide_env {
662 if let Some(env) = env {
663 parts.push(format!("[env: {env}]"));
664 }
665 parts.extend(
666 env_fallback
667 .iter()
668 .map(|env| format!("[env fallback: {env}]")),
669 );
670 parts.extend(
671 deprecated_env
672 .iter()
673 .map(|env| format!("[deprecated env: {env}]")),
674 );
675 }
676 if !hide_default_value && !default.is_empty() {
677 parts.push(format!("(default: {})", default.join(", ")));
678 }
679 parts
680}
681
682#[derive(Clone, Copy)]
684struct Column {
685 width: usize,
687 col: usize,
689 long: bool,
691 next_line: bool,
693}
694
695const BLOCK_INDENT: usize = 4;
697
698impl Column {
699 fn usage_overflows(&self, usage_width: usize) -> bool {
700 !self.next_line && usage_width > self.col
701 }
702
703 fn description_indent(&self) -> usize {
704 2 + self.col + 2
705 }
706
707 fn inline_help_start(&self, usage_width: usize) -> usize {
708 if self.usage_overflows(usage_width) {
709 2 + usage_width + 2
710 } else {
711 self.description_indent()
712 }
713 }
714
715 fn can_inline(&self, usage_width: usize) -> bool {
717 if self.next_line {
718 return false;
719 }
720 let minimum = if self.usage_overflows(usage_width) {
721 crate::docs::layout::MIN_INLINE_HELP_WIDTH
722 } else {
723 10
724 };
725 self.width
726 .saturating_sub(self.inline_help_start(usage_width))
727 >= minimum
728 }
729
730 fn annotation_indent(&self, reached_column: bool) -> String {
737 let indent = if reached_column {
738 self.description_indent()
739 } else {
740 self.block_indent()
741 };
742 " ".repeat(indent)
743 }
744
745 fn block_indent(&self) -> usize {
748 let description = self.description_indent();
749 if !self.next_line && self.width.saturating_sub(description) >= 10 {
750 description
751 } else {
752 BLOCK_INDENT
753 }
754 }
755}
756
757const HELP_SUBCOMMAND: &str = "help";
759const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
760
761fn lay_out_commands(
766 commands: &mut [crate::docs::models::HelpCommand],
767 terminal_width: usize,
768 col: usize,
769 next_line: bool,
770) {
771 let column = Column {
772 width: terminal_width,
773 col,
774 long: false,
775 next_line,
776 };
777 for command in commands {
778 command.usage_col_width = col;
779 command.row = command_row(command);
780 command.help_rendered = None;
781 command.help_is_multiline = false;
782 let usage_width = crate::docs::layout::visible_width(&command.name);
783 if let Some(row) = command.row.as_deref() {
784 if column.can_inline(usage_width) {
785 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
786 row,
787 terminal_width,
788 column.inline_help_start(usage_width),
789 column.description_indent(),
790 );
791 if !rendered.is_empty() {
792 command.help_rendered = Some(rendered);
793 command.help_is_multiline = is_multiline;
794 }
795 } else if let Some(row) = command.row.as_mut() {
796 let indent = column.block_indent();
797 *row = indent_text(
798 &crate::docs::layout::render_indented_text(row, terminal_width, indent),
799 indent,
800 );
801 }
802 }
803 }
804}
805
806fn command_row(cmd: &crate::docs::models::HelpCommand) -> Option<String> {
813 let mut parts = Vec::new();
814 let summary = summarize(cmd.help.as_deref()).or_else(|| {
817 summarize(
818 cmd.help_long
819 .as_deref()
820 .and_then(|help| help.lines().next()),
821 )
822 });
823 if let Some(summary) = summary {
824 parts.push(summary.to_string());
825 }
826 if !cmd.aliases.is_empty() {
827 parts.push(format!("[aliases: {}]", cmd.aliases.join(", ")));
828 }
829 if let Some(label) = deprecation_label(
830 cmd.deprecated.as_deref(),
831 cmd.deprecated_warn_at.as_deref(),
832 cmd.deprecated_remove_at.as_deref(),
833 ) {
834 parts.push(label);
835 }
836 (!parts.is_empty()).then(|| parts.join(" "))
837}
838
839fn summarize(text: Option<&str>) -> Option<&str> {
841 text.map(str::trim_end).filter(|text| !text.is_empty())
842}
843
844fn deprecation_label(
846 message: Option<&str>,
847 warn_at: Option<&str>,
848 remove_at: Option<&str>,
849) -> Option<String> {
850 if message.is_none() && warn_at.is_none() && remove_at.is_none() {
851 return None;
852 }
853 let mut parts = Vec::new();
854 if let Some(message) = message {
855 parts.push(message.to_string());
856 }
857 if let Some(at) = warn_at {
858 parts.push(format!("warns at {at}"));
859 }
860 if let Some(at) = remove_at {
861 parts.push(format!("removed at {at}"));
862 }
863 Some(format!("[deprecated: {}]", parts.join("; ")))
864}
865
866fn render_row(
868 name: &str,
869 row: &str,
870 terminal_width: usize,
871 col: usize,
872 next_line_help: bool,
873) -> String {
874 if !next_line_help {
875 if crate::docs::layout::visible_width(name) <= col {
876 let (rendered, _) = crate::docs::layout::render_help_text(row, terminal_width, col);
877 if !rendered.is_empty() {
878 return format!(" {name:<col$} {rendered}");
879 }
880 } else if !row.contains('\n') {
881 return format!(
882 " {name}\n {}",
883 crate::docs::layout::render_block_text(row, terminal_width)
884 );
885 }
886 }
887 format!(" {name}\n {row}")
888}
889
890fn inherited_flags(
899 spec: &Spec,
900 cmd: &SpecCommand,
901 full_cmd: &[String],
902 long_help: bool,
903) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
904 let mut ancestors: Vec<&SpecCommand> = Vec::new();
906 let mut at = &spec.cmd;
907 for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
908 ancestors.push(at);
909 let Some(next) = at.subcommands.get(name) else {
910 return (Vec::new(), Vec::new());
911 };
912 at = next;
913 }
914 if !full_cmd.is_empty() {
915 ancestors.push(at);
916 }
917
918 let forms = |f: &crate::SpecFlag| -> Vec<String> {
931 f.long
932 .iter()
933 .map(|l| format!("--{l}"))
934 .chain(f.short.iter().map(|s| format!("-{s}")))
935 .collect()
936 };
937 let every_form: Vec<String> = cmd
938 .flags
939 .iter()
940 .chain(
941 ancestors
942 .iter()
943 .flat_map(|a| a.flags.iter())
944 .filter(|f| f.global),
945 )
946 .flat_map(&forms)
947 .collect();
948
949 let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
950 let mut taken_negations: Vec<String> =
951 cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
952 let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
953 for ancestor in ancestors.iter().rev() {
954 for f in ancestor.flags.iter().filter(|f| f.global) {
955 let long = f
956 .long
957 .iter()
958 .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
959 .cloned();
960 let short = f
961 .short
962 .iter()
963 .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
964 .copied();
965 let mine = forms(f);
966 let negate = f.negate.as_ref().is_some_and(|n| {
967 !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
968 });
969 taken.extend(forms(f));
972 taken_negations.extend(f.negate.clone());
973 if f.hide
974 || if long_help {
975 f.hide_long_help
976 } else {
977 f.hide_short_help
978 }
979 || (long.is_none() && short.is_none() && !negate)
980 {
981 continue;
982 }
983 keep.push((f, long, short, negate));
984 }
985 }
986 let shown: Vec<crate::docs::models::SpecFlag> = ancestors
987 .iter()
988 .flat_map(|a| a.flags.iter())
989 .filter_map(|f| {
990 keep.iter()
991 .find(|(k, _, _, _)| std::ptr::eq(*k, f))
992 .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
993 })
994 .map(|(f, long, short, negate)| {
995 let mut shown = f.clone();
998 shown.long = long.into_iter().collect();
999 shown.short = short.into_iter().collect();
1000 if !negate {
1001 shown.negate = None;
1002 }
1003 shown.usage = shown.usage();
1004 crate::docs::models::SpecFlag::from(&shown)
1005 })
1006 .collect();
1007 taken.extend(taken_negations);
1011 (shown, taken)
1012}
1013
1014fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
1026 let mut visible = cmd.clone();
1027 visible.flags.retain(|flag| {
1028 !flag.hide
1029 && if long {
1030 !flag.hide_long_help
1031 } else {
1032 !flag.hide_short_help
1033 }
1034 });
1035 visible.args.retain(|arg| {
1036 !arg.hide
1037 && if long {
1038 !arg.hide_long_help
1039 } else {
1040 !arg.hide_short_help
1041 }
1042 });
1043 visible.subcommands.retain(|_, sub| !sub.hide);
1044 if visible.flatten_help {
1050 for sub in visible.subcommands.values_mut() {
1051 *sub = without_hidden(sub, long);
1052 }
1053 }
1054 visible
1055}
1056
1057static TERA: LazyLock<Tera> = LazyLock::new(|| {
1058 let mut tera = Tera::default();
1059
1060 tera.register_filter(
1062 "ljust",
1063 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1064 let value = value.as_str().unwrap_or("");
1065 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
1066 Ok(format!("{:<width$}", value, width = width))
1067 },
1068 );
1069 tera.register_filter(
1070 "default",
1071 |value: &tera::Value,
1072 kwargs: tera::Kwargs,
1073 _: &tera::State|
1074 -> tera::TeraResult<tera::Value> {
1075 let default_val = kwargs.must_get::<tera::Value>("value")?;
1076 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
1077 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
1078 Ok(default_val)
1079 } else {
1080 Ok(value.clone())
1081 }
1082 },
1083 );
1084 tera.register_filter(
1085 "terminal_wrap",
1086 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1087 let value = value.as_str().unwrap_or("");
1088 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1089 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1090 Ok(crate::docs::layout::render_indented_text(
1091 value, width, indent,
1092 ))
1093 },
1094 );
1095 tera.register_filter(
1096 "terminal_label",
1097 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1098 let value = value.as_str().unwrap_or("");
1099 let label = args.must_get::<String>("label")?;
1100 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1101 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1102 Ok(crate::docs::layout::render_labelled_text(
1103 &label, value, width, indent,
1104 ))
1105 },
1106 );
1107 tera.register_filter(
1108 "terminal_annotation",
1109 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1110 let body = if let Some(values) = value.as_array() {
1111 values
1112 .iter()
1113 .filter_map(tera::Value::as_str)
1114 .collect::<Vec<_>>()
1115 .join(", ")
1116 } else {
1117 value.as_str().unwrap_or("").to_string()
1118 };
1119 let label = args.get::<String>("label")?.unwrap_or_default();
1120 let suffix = args.get::<String>("suffix")?.unwrap_or_default();
1121 let indent = args
1122 .get::<String>("indent")?
1123 .unwrap_or_default()
1124 .chars()
1125 .count();
1126 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1127 let rendered = crate::docs::layout::render_indented_text(
1128 &format!("{label}{body}{suffix}"),
1129 width,
1130 indent,
1131 );
1132 Ok(indent_text(&rendered, indent))
1133 },
1134 );
1135 tera.register_filter(
1136 "terminal_deprecation",
1137 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1138 let field = |name| value.get_from_path(name).and_then(tera::Value::as_str);
1139 let Some(label) = deprecation_label(
1140 field("deprecated"),
1141 field("deprecated_warn_at"),
1142 field("deprecated_remove_at"),
1143 ) else {
1144 return Ok(String::new());
1145 };
1146 let indent = args
1147 .get::<String>("indent")?
1148 .unwrap_or_default()
1149 .chars()
1150 .count();
1151 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1152 let rendered = crate::docs::layout::render_indented_text(&label, width, indent);
1153 Ok(indent_text(&rendered, indent))
1154 },
1155 );
1156
1157 #[rustfmt::skip]
1158 tera.add_raw_templates([
1159 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
1160 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
1161 ]).unwrap();
1162
1163 tera
1164});
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169 use insta::assert_snapshot;
1170
1171 #[test]
1172 fn flag_aliases_do_not_leak_into_interactive_help() {
1173 let spec = crate::spec! { r#"
1174bin "ex"
1175flag "-t -f --tail --follow" help="Follow output"
1176 "# }
1177 .unwrap();
1178
1179 for long in [false, true] {
1180 let page = super::render_help(&spec, &spec.cmd, long);
1181 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1182 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1183 }
1184 }
1185
1186 #[test]
1187 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1188 let spec = crate::spec! { r#"
1193bin "ex"
1194flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1195cmd inner help="a command" {
1196 flag "--plain" help="its own"
1197}
1198 "# }
1199 .unwrap();
1200
1201 let inner = spec.cmd.subcommands.get("inner").expect("inner");
1202 for long in [false, true] {
1203 let page = super::render_help(&spec, inner, long);
1204 assert!(
1205 !page.contains("--help"),
1206 "long={long}: a hidden ancestor binds this:\n{page}"
1207 );
1208 assert!(page.contains("-h"), "long={long}:\n{page}");
1210 }
1211 }
1212
1213 #[test]
1214 fn a_long_beats_a_negation_however_far_away_it_is() {
1215 let spec = crate::spec! { r#"
1221bin "ex"
1222flag "--no-cache" global=#true help="the root's plain long"
1223flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1224cmd narrow help="a command" {
1225 flag "--cache" negate="--no-cache" help="its own, with a negation"
1226 flag "--tint" negate="--no-colour" help="claims the root's negation"
1227}
1228 "# }
1229 .unwrap();
1230
1231 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1232 for long in [false, true] {
1233 let page = super::render_help(&spec, narrow, long);
1234 assert!(
1235 page.contains("--no-cache"),
1236 "long={long}: a long beats a negation, so this still binds here:\n{page}"
1237 );
1238 assert!(page.contains("--colour"), "long={long}:\n{page}");
1241 let global = page
1242 .split_once("Global flags:")
1243 .expect("a global section")
1244 .1;
1245 assert!(
1246 !global.contains("--colour / --no-colour"),
1247 "long={long}: the nearer negation owns that spelling:\n{page}"
1248 );
1249 }
1250 }
1251
1252 #[test]
1253 fn a_description_of_only_spaces_is_no_description() {
1254 let spec = crate::spec! { r#"
1261bin "ex"
1262flag "--blank" help=" "
1263flag "--plain" help="plain"
1264 "# }
1265 .unwrap();
1266
1267 for long in [false, true] {
1268 let page = super::render_help(&spec, &spec.cmd, long);
1269 let listing = page.split_once("\nFlags:").expect("a flags section").1;
1272 let line = listing
1273 .lines()
1274 .find(|l| l.contains("--blank"))
1275 .unwrap_or_else(|| panic!("long={long}: {page}"));
1276 assert_eq!(
1277 line,
1278 line.trim_end(),
1279 "long={long}: trailing space on {line:?}"
1280 );
1281 }
1282 }
1283
1284 #[test]
1285 fn test_render_help_omits_hidden_entries() {
1286 let spec = crate::spec! { r#"
1287bin "ex"
1288flag "--visible" help="shown"
1289flag "--secret" hide=#true help="hidden"
1290flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1291arg "[SHOWN]" help="an arg"
1292arg "[HIDDEN]" hide=#true help="a hidden arg"
1293cmd open help="a command"
1294cmd sneaky hide=#true help="a hidden command"
1295 "# }
1296 .unwrap();
1297
1298 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1303 Usage: ex [--visible] [SHOWN] <SUBCOMMAND>
1304
1305 Commands:
1306 open a command
1307 help Print this message or the help of the given subcommand(s)
1308
1309 Arguments:
1310 [SHOWN] an arg
1311
1312 Flags:
1313 --visible shown
1314 -h, --help Print help
1315 ");
1316 }
1317
1318 #[test]
1319 fn test_render_help_groups_by_heading() {
1320 let spec = crate::spec! { r#"
1321bin "testcli"
1322flag "--verbose" help="Verbose output"
1323flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1324flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1325flag "--jobs <n>" help="How many at once" help_heading="Performance"
1326arg "<file>" help="The file"
1327arg "<mode>" help="How to run" help_heading="Behaviour"
1328 "# }
1329 .unwrap();
1330
1331 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1334 Usage: testcli [FLAGS] <file> <mode>
1335
1336 Arguments:
1337 <file> The file
1338
1339 Behaviour:
1340 <mode> How to run
1341
1342 Flags:
1343 --verbose Verbose output
1344 -h, --help Print help
1345
1346 Filtering:
1347 --filter <pattern> Only matching
1348 --exclude <pattern> Skip matching
1349
1350 Performance:
1351 --jobs <n> How many at once
1352 ");
1353 }
1354
1355 #[test]
1356 fn test_render_help_with_only_headed_flags() {
1357 let spec = crate::spec! { r#"
1360bin "testcli"
1361flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1362 "# }
1363 .unwrap();
1364
1365 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1366 Usage: testcli [--filter <pattern>]
1367
1368 Flags:
1369 -h, --help Print help
1370
1371 Filtering:
1372 --filter <pattern> Only matching
1373 ");
1374 }
1375
1376 #[test]
1377 fn test_render_help_with_env() {
1378 let spec = crate::spec! { r#"
1379bin "testcli"
1380flag "--color" env="MYCLI_COLOR" help="Enable color output"
1381flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1382flag "--debug" help="Debug mode"
1383 "# }
1384 .unwrap();
1385
1386 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1387 Usage: testcli [FLAGS]
1388
1389 Flags:
1390 --color Enable color output [env: MYCLI_COLOR]
1391 --verbose Verbose output [env: MYCLI_VERBOSE]
1392 --debug Debug mode
1393 -h, --help Print help
1394 ");
1395
1396 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1397 Usage: testcli [FLAGS]
1398
1399 Flags:
1400 --color Enable color output
1401 [env: MYCLI_COLOR]
1402 --verbose Verbose output
1403 [env: MYCLI_VERBOSE]
1404 --debug Debug mode
1405 -h, --help Print help
1406 ");
1407 }
1408
1409 #[test]
1410 fn test_render_help_with_arg_env() {
1411 let spec = crate::spec! { r#"
1412bin "testcli"
1413arg "<input>" env="MY_INPUT" help="Input file"
1414arg "<output>" env="MY_OUTPUT" help="Output file"
1415arg "<extra>" help="Extra arg without env"
1416arg "[default]" help="Arg with default value" default="default value"
1417 "# }
1418 .unwrap();
1419
1420 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1421 Usage: testcli <ARGS>…
1422
1423 Arguments:
1424 <input> Input file [env: MY_INPUT]
1425 <output> Output file [env: MY_OUTPUT]
1426 <extra> Extra arg without env
1427 [default] Arg with default value (default: default value)
1428
1429 Flags:
1430 -h, --help Print help
1431 ");
1432
1433 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1434 Usage: testcli <ARGS>…
1435
1436 Arguments:
1437 <input> Input file
1438 [env: MY_INPUT]
1439 <output> Output file
1440 [env: MY_OUTPUT]
1441 <extra> Extra arg without env
1442 [default] Arg with default value
1443 (default: default value)
1444
1445 Flags:
1446 -h, --help Print help
1447 ");
1448 }
1449
1450 #[test]
1451 fn test_render_help_with_negated_flag() {
1452 let spec = crate::spec! { r#"
1453bin "testcli"
1454flag "--compress" negate="--no-compress" default=#true help="Compress output"
1455flag "--verbose" help="Verbose output"
1456 "# }
1457 .unwrap();
1458
1459 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1460 Usage: testcli [--compress] [--verbose]
1461
1462 Flags:
1463 --compress / --no-compress Compress output (default: true)
1464 --verbose Verbose output
1465 -h, --help Print help
1466 ");
1467
1468 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1469 Usage: testcli [--compress] [--verbose]
1470
1471 Flags:
1472 --compress / --no-compress Compress output
1473 (default: true)
1474 --verbose Verbose output
1475 -h, --help Print help
1476 ");
1477 }
1478
1479 #[test]
1480 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1481 let spec = crate::spec! { r#"
1482bin "testcli"
1483flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1484 choices {
1485 choice "fast"
1486 choice "slow"
1487 }
1488}
1489flag "--short-only" help="short" hide_long_help=#true
1490flag "--long-only" help="long" hide_short_help=#true
1491arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1492 "# }
1493 .unwrap();
1494
1495 let short = render_help(&spec, &spec.cmd, false);
1496 assert!(short.contains("--mode <mode>"), "{short}");
1497 assert!(short.contains("--short-only"), "{short}");
1498 assert!(!short.contains("--long-only"), "{short}");
1499 assert!(
1500 !short.contains("MODE") && !short.contains("fast, slow"),
1501 "{short}"
1502 );
1503 assert!(
1504 !short.contains("default: fast") && !short.contains("default: file"),
1505 "{short}"
1506 );
1507
1508 let long = render_help(&spec, &spec.cmd, true);
1509 assert!(long.contains("--long-only"), "{long}");
1510 assert!(!long.contains("--short-only"), "{long}");
1511 assert!(
1512 !long.contains("MODE") && !long.contains("possible values"),
1513 "{long}"
1514 );
1515
1516 let rendered = spec.to_string();
1517 let reparsed: crate::Spec = rendered.parse().unwrap();
1518 assert!(reparsed.cmd.flags[0].hide_default_value);
1519 assert!(reparsed.cmd.flags[0].hide_env);
1520 assert!(reparsed.cmd.flags[0].hide_possible_values);
1521 }
1522
1523 #[test]
1524 fn a_help_template_reorders_omits_and_wraps_the_sections() {
1525 let spec = crate::spec! { r#"
1529bin "ex"
1530about "An example"
1531help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1532flag "--force" help="Do it anyway"
1533arg "<file>" help="Which file"
1534cmd "run" help="Run it"
1535 "# }
1536 .unwrap();
1537
1538 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1539 An example
1540
1541 Usage: ex [--force] <file> <SUBCOMMAND>
1542
1543 Flags:
1544 --force Do it anyway
1545 -h, --help Print help
1546
1547 Arguments:
1548 <file> Which file
1549
1550 -- ask a person --
1551 ");
1552 }
1553
1554 #[test]
1555 fn a_template_places_the_sections_a_page_actually_has() {
1556 let spec = crate::spec! { r#"
1562bin "ex"
1563version "1.2.3"
1564about "An example"
1565after_help "Read the docs."
1566help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1567flag "--force" help="Do it anyway"
1568cmd "run" help="Run it"
1569 "# }
1570 .unwrap();
1571
1572 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1573 Usage: ex [--force] <SUBCOMMAND>
1574
1575 Flags:
1576 --force Do it anyway
1577 -h, --help Print help
1578 -V, --version Print version
1579
1580 Commands:
1581 run Run it
1582 help Print this message or the help of the given subcommand(s)
1583
1584 Read the docs.
1585
1586 ex 1.2.3
1587 An example
1588 ");
1589 }
1590
1591 #[test]
1592 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1593 let spec = crate::spec! { r#"
1596bin "ex"
1597flatten_help #true
1598help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1599cmd "run" help="Run it" {
1600 flag "--dry-run" help="Only show changes"
1601}
1602 "# }
1603 .unwrap();
1604
1605 let page = render_help(&spec, &spec.cmd, false);
1606 assert!(
1607 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1608 "{page}"
1609 );
1610 assert!(page.contains("--dry-run"), "{page}");
1611 }
1612
1613 #[test]
1614 fn test_render_help_with_before_after_help() {
1615 let spec = crate::spec! { r#"
1616bin "testcli"
1617before_help "This text appears before the help"
1618after_help "This text appears after the help"
1619flag "--verbose" help="Enable verbose output"
1620 "# }
1621 .unwrap();
1622
1623 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1624 This text appears before the help
1625
1626 Usage: testcli [--verbose]
1627
1628 Flags:
1629 --verbose Enable verbose output
1630 -h, --help Print help
1631
1632 This text appears after the help
1633 ");
1634 }
1635
1636 #[test]
1637 fn test_render_help_with_before_after_help_long() {
1638 let spec = crate::spec! { r#"
1639bin "testcli"
1640before_help "short before"
1641before_help_long "This is the long version of before help"
1642after_help "short after"
1643after_help_long "This is the long version of after help"
1644flag "--verbose" help="Enable verbose output"
1645 "# }
1646 .unwrap();
1647
1648 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1649 short before
1650
1651 Usage: testcli [--verbose]
1652
1653 Flags:
1654 --verbose Enable verbose output
1655 -h, --help Print help
1656
1657 short after
1658 ");
1659
1660 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1661 This is the long version of before help
1662
1663 Usage: testcli [--verbose]
1664
1665 Flags:
1666 --verbose Enable verbose output
1667 -h, --help Print help
1668
1669 This is the long version of after help
1670 ");
1671 }
1672
1673 #[test]
1674 fn root_help_stays_on_the_root_page() {
1675 let spec = crate::spec! { r#"
1676bin "testcli"
1677author "Root Author"
1678license "MIT"
1679before_help "root before"
1680before_help_long "root before long"
1681after_help "root after"
1682after_help_long "root after long"
1683example "testcli --verbose"
1684cmd "run" help="run it" {
1685 after_help "run after"
1686 after_long_help "run after long"
1687 example "testcli run"
1688 cmd "now" help="run it now"
1689}
1690 "# }
1691 .unwrap();
1692 let run = spec.cmd.subcommands.get("run").expect("run command");
1693 let now = run.subcommands.get("now").expect("now command");
1694
1695 for long in [false, true] {
1696 let page = render_help(&spec, now, long);
1697 for root_only in [
1698 "root before",
1699 "root after",
1700 "$ testcli --verbose",
1701 "run after",
1702 "$ testcli run",
1703 "Root Author",
1704 "License: MIT",
1705 ] {
1706 assert!(
1707 !page.contains(root_only),
1708 "long={long}, inherited ancestor metadata {root_only:?}:\n{page}"
1709 );
1710 }
1711 }
1712
1713 let short = render_help(&spec, run, false);
1714 let long = render_help(&spec, run, true);
1715 assert!(short.contains("run after"), "{short}");
1716 assert!(short.contains("$ testcli run"), "{short}");
1717 assert!(long.contains("run after long"), "{long}");
1718 assert!(long.contains("$ testcli run"), "{long}");
1719 }
1720
1721 #[test]
1722 fn test_render_help_with_examples() {
1723 let spec = crate::spec! { r#"
1724bin "testcli"
1725flag "--verbose" help="Enable verbose output"
1726example "testcli --verbose" header="Run with verbose output"
1727example "testcli" header="Run normally" help="Just runs the tool"
1728 "# }
1729 .unwrap();
1730
1731 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1732 Usage: testcli [--verbose]
1733
1734 Flags:
1735 --verbose Enable verbose output
1736 -h, --help Print help
1737
1738 Examples:
1739 Run with verbose output:
1740 $ testcli --verbose
1741 Run normally:
1742 $ testcli
1743 ");
1744
1745 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1746 Usage: testcli [--verbose]
1747
1748 Flags:
1749 --verbose Enable verbose output
1750 -h, --help Print help
1751
1752 Examples:
1753 Run with verbose output:
1754 $ testcli --verbose
1755 Run normally:
1756 Just runs the tool
1757 $ testcli
1758 ");
1759 }
1760
1761 #[test]
1762 fn test_render_help_with_version() {
1763 let spec = crate::spec! { r#"
1764bin "testcli"
1765name "TestCLI"
1766version "1.2.3"
1767flag "--verbose" help="Enable verbose output"
1768 "# }
1769 .unwrap();
1770
1771 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1772 TestCLI 1.2.3
1773 Usage: testcli [--verbose]
1774
1775 Flags:
1776 --verbose Enable verbose output
1777 -h, --help Print help
1778 -V, --version Print version
1779 ");
1780 }
1781
1782 #[test]
1783 fn test_render_help_with_only_long_version() {
1784 let spec = crate::spec! { r#"
1785bin "testcli"
1786long_version "1.2.3\ncommit abc123"
1787flag "--verbose" help="Enable verbose output"
1788 "# }
1789 .unwrap();
1790
1791 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1792 Usage: testcli [--verbose]
1793
1794 Flags:
1795 --verbose Enable verbose output
1796 -h, --help Print help
1797 -V, --version Print version
1798 ");
1799 }
1800
1801 #[test]
1802 fn test_render_help_omits_help_when_disabled() {
1803 let spec = crate::spec! { r#"
1807bin "testcli"
1808version "1.2.3"
1809disable_help #true
1810flag "--verbose" help="Enable verbose output"
1811 "# }
1812 .unwrap();
1813
1814 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1815 testcli 1.2.3
1816 Usage: testcli [--verbose]
1817
1818 Flags:
1819 --verbose Enable verbose output
1820 -V, --version Print version
1821 ");
1822 }
1823
1824 #[test]
1825 fn test_render_help_with_author_license() {
1826 let spec = crate::spec! { r#"
1827bin "testcli"
1828author "Test Author"
1829license "MIT"
1830flag "--verbose" help="Enable verbose output"
1831 "# }
1832 .unwrap();
1833
1834 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1836 Usage: testcli [--verbose]
1837
1838 Flags:
1839 --verbose Enable verbose output
1840 -h, --help Print help
1841 ");
1842
1843 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1845 Usage: testcli [--verbose]
1846
1847 Flags:
1848 --verbose Enable verbose output
1849 -h, --help Print help
1850
1851 Author: Test Author
1852 License: MIT
1853 ");
1854 }
1855
1856 #[test]
1857 fn test_render_help_with_deprecated_command() {
1858 let spec = crate::spec! { r#"
1859bin "testcli"
1860flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
1861cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
1862cmd "new-cmd" help="Do something better"
1863 "# }
1864 .unwrap();
1865
1866 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1867 Usage: testcli [--old] <SUBCOMMAND>
1868
1869 Commands:
1870 new-cmd Do something better
1871 old-cmd Do something [deprecated: use new-cmd instead; warns at 6.2; removed
1872 at 7.0]
1873 help Print this message or the help of the given subcommand(s)
1874
1875 Flags:
1876 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
1877 -h, --help Print help
1878 ");
1879 }
1880
1881 #[test]
1882 fn deprecation_milestones_do_not_need_a_message() {
1883 let spec = crate::spec! { r#"
1884bin "testcli"
1885flag "--old" help="Old switch" deprecated_remove_at="7.0"
1886cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
1887 "# }
1888 .unwrap();
1889
1890 let page = render_help(&spec, &spec.cmd, false);
1891 assert!(
1892 page.contains("old-cmd Do something [deprecated: warns at 6.2]"),
1893 "{page}"
1894 );
1895 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
1896 assert!(!page.contains("[deprecated:;"), "{page}");
1897 }
1898
1899 #[test]
1900 fn test_render_help_with_subcommand_presentation() {
1901 let spec = crate::spec! { r#"
1902bin "testcli"
1903subcommand_help_heading "Actions"
1904subcommand_value_name "ACTION"
1905cmd "run" help="Run it\n"
1906 "# }
1907 .unwrap();
1908
1909 let page = render_help(&spec, &spec.cmd, false);
1910 assert!(page.contains("Usage: testcli <ACTION>"), "{page}");
1911 assert!(page.contains("\nActions:\n"), "{page}");
1912 }
1913
1914 #[test]
1915 fn test_render_help_honors_explicit_display_order() {
1916 let spec = crate::spec! { r#"
1917bin "testcli"
1918flag "--unset" help="Unordered"
1919flag "--later" help="Later" display_order=20
1920flag "--first" help="First" display_order=10
1921cmd "zulu" help="Unordered"
1922cmd "later" help="Later" display_order=20
1923cmd "first" help="First" display_order=10
1924cmd "alpha" help="Unordered"
1925 "# }
1926 .unwrap();
1927
1928 let page = render_help(&spec, &spec.cmd, false);
1929 let commands = page.split_once("\nCommands:\n").unwrap().1;
1930 assert!(
1931 commands.find("first").unwrap() < commands.find("later").unwrap()
1932 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
1933 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
1934 "{page}"
1935 );
1936 let flags = page.split_once("\nFlags:\n").unwrap().1;
1937 assert!(
1938 flags.find("--first").unwrap() < flags.find("--later").unwrap()
1939 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
1940 "{page}"
1941 );
1942 }
1943
1944 #[test]
1945 fn test_render_help_groups_subcommands_by_heading() {
1946 let spec = crate::spec! { r#"
1947bin "testcli"
1948cmd "run" help="Run it" help_heading="Core commands"
1949cmd "clean" help="Remove old state" help_heading="Maintenance"
1950cmd "status" help="Show status" help_heading="Commands"
1951 "# }
1952 .unwrap();
1953
1954 for page in [
1955 render_help(&spec, &spec.cmd, false),
1956 render_help(&spec, &spec.cmd, true),
1957 ] {
1958 let commands = page.find("\nCommands:\n").expect("default command section");
1959 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
1960 let core = page.find("\nCore commands:\n").expect("core section");
1961 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
1962 assert!(commands < core && commands < maintenance, "{page}");
1963 let default_end = core.min(maintenance);
1964 assert!(page[commands..default_end].contains("status"), "{page}");
1965 assert!(page[commands..default_end].contains("help"), "{page}");
1966 assert!(page[core..].contains("run"), "{page}");
1967 assert!(page[maintenance..].contains("clean"), "{page}");
1968 }
1969 }
1970
1971 #[test]
1972 fn test_render_help_with_next_line_layout() {
1973 let spec = crate::spec! { r#"
1974bin "testcli"
1975next_line_help #true
1976arg "<input>" help="Input file" env="INPUT" default="fast" {
1977 choices {
1978 choice "fast"
1979 choice "slow"
1980 }
1981}
1982flag "--verbose" help="Enable verbose output"
1983cmd "run" help="Run it"
1984 "# }
1985 .unwrap();
1986
1987 let short = render_help(&spec, &spec.cmd, false);
1988 assert!(!short.contains(" Run it\n\n help"), "{short}");
1989 for page in [short, render_help(&spec, &spec.cmd, true)] {
1990 assert!(page.contains(" [input]\n Input file"), "{page}");
1991 assert!(
1992 page.contains("--verbose\n Enable verbose output"),
1993 "{page}"
1994 );
1995 assert!(
1996 page.contains(
1997 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
1998 ),
1999 "{page}"
2000 );
2001 assert!(page.contains(" run\n Run it"), "{page}");
2002 }
2003 }
2004
2005 #[test]
2006 fn flatten_help_expands_subcommands_instead_of_listing_them() {
2007 let spec = crate::spec! { r#"
2008bin "testcli"
2009flatten_help #true
2010next_line_help #true
2011cmd "run" help="Run it" {
2012 arg "<task>" help="Task name" env="TASK" default="build" {
2013 choices {
2014 choice "build"
2015 choice "test"
2016 }
2017 }
2018 flag "--dry-run" help="Only show changes"
2019 flatten_help #true
2020 cmd "nested" help="Nested operation" {
2021 flag "--deep" help="Deep option"
2022 }
2023}
2024 "# }
2025 .unwrap();
2026
2027 for page in [
2028 render_help(&spec, &spec.cmd, false),
2029 render_help(&spec, &spec.cmd, true),
2030 ] {
2031 assert!(
2032 page.contains("Usage: testcli\n testcli run"),
2033 "{page}"
2034 );
2035 assert!(!page.contains("\nCommands:\n"), "{page}");
2036 assert!(page.contains("\nrun:\nRun it"), "{page}");
2037 assert!(page.contains("[task]"), "{page}");
2038 assert!(page.contains("--dry-run"), "{page}");
2039 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
2040 assert!(page.contains("--deep"), "{page}");
2041 assert!(
2042 page.contains(
2043 " [possible values: build, test]\n [env: TASK]\n (default: build)"
2044 ),
2045 "{page}"
2046 );
2047 }
2048 }
2049
2050 #[test]
2051 fn styled_help_colours_semantics_and_template_styles() {
2052 let mut spec = crate::spec! { r#"
2053bin "testcli"
2054help_template "{$bright-blue}My tool{/$}\n\n{{usage}}\n\n{{flags}}\n\n{{after_help}}"
2055flag "--output <FILE>" help="Write **the file**"
2056 "# }
2057 .unwrap();
2058 spec.cmd.after_help_long = Some(
2059 "\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n \u{1b}[1mtestcli run\u{1b}[22m"
2060 .to_string(),
2061 );
2062
2063 let plain = render_help(&spec, &spec.cmd, true);
2064 assert!(plain.contains("My tool"), "{plain}");
2065 assert!(plain.contains("Examples:\n\n testcli run"), "{plain}");
2066 assert!(!plain.contains('\u{1b}'), "{plain:?}");
2067
2068 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2069 assert!(
2070 coloured.contains("\u{1b}[94mMy tool\u{1b}[0m"),
2071 "{coloured:?}"
2072 );
2073 assert!(
2074 coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"),
2075 "{coloured:?}"
2076 );
2077 assert!(
2078 coloured.contains("\u{1b}[1;32m--output\u{1b}[0m"),
2079 "{coloured:?}"
2080 );
2081 assert!(
2082 coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"),
2083 "{coloured:?}"
2084 );
2085 assert!(
2086 coloured.contains("\u{1b}[1mthe file\u{1b}[22m"),
2087 "{coloured:?}"
2088 );
2089 assert!(
2090 coloured.contains("\u{1b}[1m\u{1b}[4mExamples:"),
2091 "{coloured:?}"
2092 );
2093 }
2094}