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(
192 &docs_cmd,
193 &inherited,
194 sections.usage,
195 !cmd.disable_help_subcommand,
196 );
197 let page = match spec
198 .help_template
199 .as_deref()
200 .filter(|t| crate::help_template::is_set(t))
201 {
202 Some(template) => {
203 crate::help_template::substitute_with_style(template, style.coloured, |name| {
204 sections
205 .named(name)
206 .map(|section| styling.apply(§ion, style))
207 })
208 }
209 None => styling.apply(§ions.concatenated(), style),
210 };
211 let page = if style.coloured {
212 std::borrow::Cow::Borrowed(page.as_str())
213 } else {
214 crate::docs::strip_ansi(&page)
215 };
216 page.trim().to_string() + "\n"
217}
218
219const MARKS: [(&str, &str); 6] = [
230 ("mark_usage", "\u{1}usage\u{1}"),
231 ("mark_commands", "\u{1}commands\u{1}"),
232 ("mark_args", "\u{1}args\u{1}"),
233 ("mark_flags", "\u{1}flags\u{1}"),
234 ("mark_flattened", "\u{1}flattened\u{1}"),
235 ("mark_after_help", "\u{1}after_help\u{1}"),
236];
237const MARK_GROUPED_ARGS: &str = "\u{1}grouped_args\u{1}";
238const MARK_GROUPED_FLAGS: &str = "\u{1}grouped_flags\u{1}";
239const MARK_GLOBAL_FLAGS: &str = "\u{1}global_flags\u{1}";
240
241struct Sections<'a> {
247 about: &'a str,
248 usage: &'a str,
249 commands: &'a str,
250 args: String,
251 flags: String,
252 grouped_args: &'a str,
253 ungrouped_args: &'a str,
254 grouped_flags: &'a str,
255 ungrouped_flags: String,
256 flattened: &'a str,
257 after_help: &'a str,
258}
259
260impl<'a> Sections<'a> {
261 fn split(rendered: &'a str) -> Self {
262 let mut rest = rendered;
263 let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
264 for (_, mark) in MARKS {
265 match rest.split_once(mark) {
269 Some((before, after)) => {
270 parts.push(before);
271 rest = after;
272 }
273 None => parts.push(""),
274 }
275 }
276 parts.push(rest);
277 let (ungrouped_args, grouped_args) = parts[3]
278 .split_once(MARK_GROUPED_ARGS)
279 .unwrap_or((parts[3], ""));
280 let (own_flags, global_flags) = parts[4]
281 .split_once(MARK_GLOBAL_FLAGS)
282 .unwrap_or((parts[4], ""));
283 let (own_ungrouped_flags, grouped_flags) = own_flags
284 .split_once(MARK_GROUPED_FLAGS)
285 .unwrap_or((own_flags, ""));
286 Self {
287 about: parts[0],
288 usage: parts[1],
289 commands: parts[2],
290 args: format!("{ungrouped_args}{grouped_args}"),
291 flags: format!("{own_ungrouped_flags}{grouped_flags}{global_flags}"),
292 grouped_args,
293 ungrouped_args,
294 grouped_flags,
295 ungrouped_flags: format!("{own_ungrouped_flags}{global_flags}"),
296 flattened: parts[5],
297 after_help: parts[6],
298 }
299 }
300
301 fn concatenated(&self) -> String {
303 [
304 self.about,
305 self.usage,
306 self.commands,
307 self.args.as_str(),
308 self.flags.as_str(),
309 self.flattened,
310 self.after_help,
311 ]
312 .concat()
313 }
314
315 fn named(&self, name: &str) -> Option<String> {
317 Some(match name {
318 "about" => self.about.trim().to_string(),
319 "usage" => self.usage.trim().to_string(),
320 "commands" => {
321 let mut out = self.commands.trim().to_string();
322 let flattened = self.flattened.trim();
323 if !flattened.is_empty() {
324 if !out.is_empty() {
325 out.push_str("\n\n");
326 }
327 out.push_str(flattened);
328 }
329 out
330 }
331 "args" => self.args.trim().to_string(),
332 "flags" => self.flags.trim().to_string(),
333 "grouped_args" => self.grouped_args.trim().to_string(),
334 "ungrouped_args" => self.ungrouped_args.trim().to_string(),
335 "grouped_flags" => self.grouped_flags.trim().to_string(),
336 "ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
337 "after_help" => self.after_help.trim().to_string(),
338 _ => return None,
339 })
340 }
341}
342
343fn supplied_flags(
357 spec: &Spec,
358 cmd: &SpecCommand,
359 ancestors_taken: &[String],
360 is_root: bool,
361) -> Vec<crate::docs::models::SpecFlag> {
362 let mut taken: Vec<String> = ancestors_taken.to_vec();
367 for f in &cmd.flags {
368 taken.extend(f.long.iter().map(|l| format!("--{l}")));
369 taken.extend(f.short.iter().map(|s| format!("-{s}")));
370 taken.extend(f.negate.clone());
372 }
373
374 let build = |name: &str, long: &str, short: char, help: &str| {
375 let long_free = !taken.contains(&format!("--{long}"));
376 let short_free = !taken.contains(&format!("-{short}"));
377 if !long_free && !short_free {
378 return None;
379 }
380 let name = if long_free { name } else { &short.to_string() };
383 let mut flag = crate::SpecFlag {
384 name: name.to_string(),
385 long: if long_free {
386 vec![long.to_string()]
387 } else {
388 vec![]
389 },
390 short: if short_free { vec![short] } else { vec![] },
391 help: Some(help.to_string()),
392 ..Default::default()
393 };
394 flag.usage = flag.usage();
395 Some(crate::docs::models::SpecFlag::from(&flag))
396 };
397
398 let mut out = Vec::new();
399 if spec.disable_help != Some(true) && !cmd.disable_help_flag {
406 out.extend(build("help", "help", 'h', "Print help"));
407 }
408 if is_root
409 && (spec.version.is_some() || spec.long_version.is_some())
410 && !cmd.disable_version_flag
411 {
412 out.extend(build("version", "version", 'V', "Print version"));
413 }
414 out
415}
416
417fn lay_out(flags: &mut [crate::docs::models::SpecFlag], column: Column) {
424 for flag in flags {
425 flag.usage_col_width = column.col;
426 flag.help_is_block =
427 !column.can_inline(crate::docs::layout::visible_width(&flag.display_usage));
428 let text = if column.long {
429 flag.help_long
430 .as_deref()
431 .or(flag.help.as_deref())
432 .map(str::to_string)
433 } else if column.next_line {
434 flag.help.clone()
435 } else {
436 with_annotations(flag.help.as_deref(), flag_annotations(flag))
437 };
438 wrap_into(
439 text,
440 column,
441 crate::docs::layout::visible_width(&flag.display_usage),
442 &mut flag.row,
443 &mut flag.help_rendered,
444 &mut flag.help_is_multiline,
445 &mut flag.ann_indent,
446 );
447 }
448}
449
450fn lay_out_args(args: &mut [crate::docs::models::SpecArg], column: Column) {
456 for arg in args {
457 arg.usage_col_width = column.col;
458 arg.help_is_block = !column.can_inline(crate::docs::layout::visible_width(&arg.usage));
459 let text = if column.long {
460 arg.help_long
461 .as_deref()
462 .or(arg.help.as_deref())
463 .map(str::to_string)
464 } else if column.next_line {
465 arg.help.clone()
466 } else {
467 with_annotations(arg.help.as_deref(), arg_annotations(arg))
468 };
469 wrap_into(
470 text,
471 column,
472 crate::docs::layout::visible_width(&arg.usage),
473 &mut arg.row,
474 &mut arg.help_rendered,
475 &mut arg.help_is_multiline,
476 &mut arg.ann_indent,
477 );
478 }
479}
480
481fn lay_out_flattened(commands: &mut [crate::docs::models::SpecCommand], width: usize, long: bool) {
482 for command in commands {
483 lay_out_group_help(&mut command.arg_groups, width);
484 lay_out_group_help(&mut command.flag_groups, width);
485 let arg_col = crate::docs::layout::usage_column_width(
486 command
487 .arg_groups
488 .iter()
489 .flat_map(|group| group.items.iter())
490 .map(|arg| arg.usage.as_str()),
491 width,
492 );
493 let arg_column = Column {
494 width,
495 col: arg_col,
496 long,
497 next_line: command.flattened_next_line_help,
498 };
499 for group in &mut command.arg_groups {
500 lay_out_args(&mut group.items, arg_column);
501 }
502
503 let flag_col = crate::docs::layout::usage_column_width(
504 command
505 .flag_groups
506 .iter()
507 .flat_map(|group| group.items.iter())
508 .map(|flag| flag.display_usage.as_str()),
509 width,
510 );
511 let flag_column = Column {
512 width,
513 col: flag_col,
514 long,
515 next_line: command.flattened_next_line_help,
516 };
517 for group in &mut command.flag_groups {
518 lay_out(&mut group.items, flag_column);
519 }
520 }
521}
522
523fn lay_out_group_help<T>(groups: &mut [crate::docs::models::Group<T>], width: usize) {
524 for group in groups {
525 group.help_rendered = group
526 .help
527 .as_deref()
528 .map(|help| crate::docs::layout::render_indented_text(help, width, 2));
529 }
530}
531
532fn wrap_into(
538 text: Option<String>,
539 column: Column,
540 usage_width: usize,
541 row: &mut Option<String>,
542 help_rendered: &mut Option<String>,
543 help_is_multiline: &mut bool,
544 ann_indent: &mut String,
545) {
546 *row = None;
547 *help_rendered = None;
548 *help_is_multiline = false;
549 *ann_indent = column.annotation_indent(column.can_inline(usage_width));
552 let Some(text) = text else { return };
553 if !column.can_inline(usage_width) {
554 let indent = column.block_indent();
555 *row = Some(indent_text(
556 &crate::docs::layout::render_indented_text(&text, column.width, indent),
557 indent,
558 ));
559 return;
560 }
561 let first_indent = column.inline_help_start(usage_width);
562 let continuation_indent = column.description_indent();
563 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
564 &text,
565 column.width,
566 first_indent,
567 continuation_indent,
568 );
569 *ann_indent = column.annotation_indent(!rendered.is_empty());
573 if !rendered.is_empty() {
574 *help_rendered = Some(rendered);
575 *help_is_multiline = is_multiline;
576 }
577 *row = Some(text);
578}
579
580fn indent_text(text: &str, indent: usize) -> String {
581 let pad = " ".repeat(indent);
582 text.lines()
583 .map(|line| {
584 if line.is_empty() {
585 String::new()
586 } else {
587 format!("{pad}{line}")
588 }
589 })
590 .collect::<Vec<_>>()
591 .join("\n")
592}
593
594fn with_annotations(help: Option<&str>, annotations: Vec<String>) -> Option<String> {
601 let mut parts = Vec::new();
602 if let Some(help) = summarize(help) {
603 parts.push(help.to_string());
604 }
605 parts.extend(annotations);
606 (!parts.is_empty()).then(|| parts.join(" "))
607}
608
609fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec<String> {
611 let mut parts = value_annotations(
612 flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()),
613 flag.hide_possible_values,
614 flag.env.as_deref(),
615 flag.hide_env,
616 &flag.env_fallback,
617 &flag.deprecated_env,
618 &flag.default,
619 flag.hide_default_value,
620 );
621 if let Some(label) = deprecation_label(
622 flag.deprecated.as_deref(),
623 flag.deprecated_warn_at.as_deref(),
624 flag.deprecated_remove_at.as_deref(),
625 ) {
626 parts.push(label);
627 }
628 parts
629}
630
631fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec<String> {
633 value_annotations(
634 arg.choices.as_ref(),
635 arg.hide_possible_values,
636 arg.env.as_deref(),
637 arg.hide_env,
638 &arg.env_fallback,
639 &arg.deprecated_env,
640 &arg.default,
641 arg.hide_default_value,
642 )
643}
644
645#[allow(clippy::too_many_arguments)]
647fn value_annotations(
648 choices: Option<&crate::SpecChoices>,
649 hide_possible_values: bool,
650 env: Option<&str>,
651 hide_env: bool,
652 env_fallback: &[String],
653 deprecated_env: &[String],
654 default: &[String],
655 hide_default_value: bool,
656) -> Vec<String> {
657 let mut parts = Vec::new();
658 if let Some(choices) = choices.filter(|_| !hide_possible_values) {
659 if !choices.choices.is_empty() {
660 parts.push(format!("[{}]", choices.choices.join(", ")));
661 }
662 if let Some(env) = choices.env() {
663 parts.push(format!("[choices env: {env}]"));
664 }
665 }
666 if !hide_env {
667 if let Some(env) = env {
668 parts.push(format!("[env: {env}]"));
669 }
670 parts.extend(
671 env_fallback
672 .iter()
673 .map(|env| format!("[env fallback: {env}]")),
674 );
675 parts.extend(
676 deprecated_env
677 .iter()
678 .map(|env| format!("[deprecated env: {env}]")),
679 );
680 }
681 if !hide_default_value && !default.is_empty() {
682 parts.push(format!("(default: {})", default.join(", ")));
683 }
684 parts
685}
686
687#[derive(Clone, Copy)]
689struct Column {
690 width: usize,
692 col: usize,
694 long: bool,
696 next_line: bool,
698}
699
700const BLOCK_INDENT: usize = 4;
702
703impl Column {
704 fn usage_overflows(&self, usage_width: usize) -> bool {
705 !self.next_line && usage_width > self.col
706 }
707
708 fn description_indent(&self) -> usize {
709 2 + self.col + 2
710 }
711
712 fn inline_help_start(&self, usage_width: usize) -> usize {
713 if self.usage_overflows(usage_width) {
714 2 + usage_width + 2
715 } else {
716 self.description_indent()
717 }
718 }
719
720 fn can_inline(&self, usage_width: usize) -> bool {
722 if self.next_line {
723 return false;
724 }
725 let minimum = if self.usage_overflows(usage_width) {
726 crate::docs::layout::MIN_INLINE_HELP_WIDTH
727 } else {
728 10
729 };
730 self.width
731 .saturating_sub(self.inline_help_start(usage_width))
732 >= minimum
733 }
734
735 fn annotation_indent(&self, reached_column: bool) -> String {
742 let indent = if reached_column {
743 self.description_indent()
744 } else {
745 self.block_indent()
746 };
747 " ".repeat(indent)
748 }
749
750 fn block_indent(&self) -> usize {
753 let description = self.description_indent();
754 if !self.next_line && self.width.saturating_sub(description) >= 10 {
755 description
756 } else {
757 BLOCK_INDENT
758 }
759 }
760}
761
762const HELP_SUBCOMMAND: &str = "help";
764const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
765
766fn lay_out_commands(
771 commands: &mut [crate::docs::models::HelpCommand],
772 terminal_width: usize,
773 col: usize,
774 next_line: bool,
775) {
776 let column = Column {
777 width: terminal_width,
778 col,
779 long: false,
780 next_line,
781 };
782 for command in commands {
783 command.usage_col_width = col;
784 command.row = command_row(command);
785 command.help_rendered = None;
786 command.help_is_multiline = false;
787 let usage_width = crate::docs::layout::visible_width(&command.name);
788 if let Some(row) = command.row.as_deref() {
789 if column.can_inline(usage_width) {
790 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
791 row,
792 terminal_width,
793 column.inline_help_start(usage_width),
794 column.description_indent(),
795 );
796 if !rendered.is_empty() {
797 command.help_rendered = Some(rendered);
798 command.help_is_multiline = is_multiline;
799 }
800 } else if let Some(row) = command.row.as_mut() {
801 let indent = column.block_indent();
802 *row = indent_text(
803 &crate::docs::layout::render_indented_text(row, terminal_width, indent),
804 indent,
805 );
806 }
807 }
808 }
809}
810
811fn command_row(cmd: &crate::docs::models::HelpCommand) -> Option<String> {
818 let mut parts = Vec::new();
819 let summary = summarize(cmd.help.as_deref()).or_else(|| {
822 summarize(
823 cmd.help_long
824 .as_deref()
825 .and_then(|help| help.lines().next()),
826 )
827 });
828 if let Some(summary) = summary {
829 parts.push(summary.to_string());
830 }
831 if !cmd.aliases.is_empty() {
832 parts.push(format!("[aliases: {}]", cmd.aliases.join(", ")));
833 }
834 if let Some(label) = deprecation_label(
835 cmd.deprecated.as_deref(),
836 cmd.deprecated_warn_at.as_deref(),
837 cmd.deprecated_remove_at.as_deref(),
838 ) {
839 parts.push(label);
840 }
841 (!parts.is_empty()).then(|| parts.join(" "))
842}
843
844fn summarize(text: Option<&str>) -> Option<&str> {
846 text.map(str::trim_end).filter(|text| !text.is_empty())
847}
848
849fn deprecation_label(
851 message: Option<&str>,
852 warn_at: Option<&str>,
853 remove_at: Option<&str>,
854) -> Option<String> {
855 if message.is_none() && warn_at.is_none() && remove_at.is_none() {
856 return None;
857 }
858 let mut parts = Vec::new();
859 if let Some(message) = message {
860 parts.push(message.to_string());
861 }
862 if let Some(at) = warn_at {
863 parts.push(format!("warns at {at}"));
864 }
865 if let Some(at) = remove_at {
866 parts.push(format!("removed at {at}"));
867 }
868 Some(format!("[deprecated: {}]", parts.join("; ")))
869}
870
871fn render_row(
873 name: &str,
874 row: &str,
875 terminal_width: usize,
876 col: usize,
877 next_line_help: bool,
878) -> String {
879 if !next_line_help {
880 if crate::docs::layout::visible_width(name) <= col {
881 let (rendered, _) = crate::docs::layout::render_help_text(row, terminal_width, col);
882 if !rendered.is_empty() {
883 return format!(" {name:<col$} {rendered}");
884 }
885 } else if !row.contains('\n') {
886 return format!(
887 " {name}\n {}",
888 crate::docs::layout::render_block_text(row, terminal_width)
889 );
890 }
891 }
892 format!(" {name}\n {row}")
893}
894
895fn inherited_flags(
904 spec: &Spec,
905 cmd: &SpecCommand,
906 full_cmd: &[String],
907 long_help: bool,
908) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
909 let mut ancestors: Vec<&SpecCommand> = Vec::new();
911 let mut at = &spec.cmd;
912 for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
913 ancestors.push(at);
914 let Some(next) = at.subcommands.get(name) else {
915 return (Vec::new(), Vec::new());
916 };
917 at = next;
918 }
919 if !full_cmd.is_empty() {
920 ancestors.push(at);
921 }
922
923 let forms = |f: &crate::SpecFlag| -> Vec<String> {
936 f.long
937 .iter()
938 .map(|l| format!("--{l}"))
939 .chain(f.short.iter().map(|s| format!("-{s}")))
940 .collect()
941 };
942 let every_form: Vec<String> = cmd
943 .flags
944 .iter()
945 .chain(
946 ancestors
947 .iter()
948 .flat_map(|a| a.flags.iter())
949 .filter(|f| f.global),
950 )
951 .flat_map(&forms)
952 .collect();
953
954 let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
955 let mut taken_negations: Vec<String> =
956 cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
957 let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
958 for ancestor in ancestors.iter().rev() {
959 for f in ancestor.flags.iter().filter(|f| f.global) {
960 let long = f
961 .long
962 .iter()
963 .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
964 .cloned();
965 let short = f
966 .short
967 .iter()
968 .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
969 .copied();
970 let mine = forms(f);
971 let negate = f.negate.as_ref().is_some_and(|n| {
972 !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
973 });
974 taken.extend(forms(f));
977 taken_negations.extend(f.negate.clone());
978 if f.hide
979 || if long_help {
980 f.hide_long_help
981 } else {
982 f.hide_short_help
983 }
984 || (long.is_none() && short.is_none() && !negate)
985 {
986 continue;
987 }
988 keep.push((f, long, short, negate));
989 }
990 }
991 let shown: Vec<crate::docs::models::SpecFlag> = ancestors
992 .iter()
993 .flat_map(|a| a.flags.iter())
994 .filter_map(|f| {
995 keep.iter()
996 .find(|(k, _, _, _)| std::ptr::eq(*k, f))
997 .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
998 })
999 .map(|(f, long, short, negate)| {
1000 let mut shown = f.clone();
1003 shown.long = long.into_iter().collect();
1004 shown.short = short.into_iter().collect();
1005 if !negate {
1006 shown.negate = None;
1007 }
1008 shown.usage = shown.usage();
1009 crate::docs::models::SpecFlag::from(&shown)
1010 })
1011 .collect();
1012 taken.extend(taken_negations);
1016 (shown, taken)
1017}
1018
1019fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
1031 let mut visible = cmd.clone();
1032 visible.flags.retain(|flag| {
1033 !flag.hide
1034 && if long {
1035 !flag.hide_long_help
1036 } else {
1037 !flag.hide_short_help
1038 }
1039 });
1040 visible.args.retain(|arg| {
1041 !arg.hide
1042 && if long {
1043 !arg.hide_long_help
1044 } else {
1045 !arg.hide_short_help
1046 }
1047 });
1048 visible.subcommands.retain(|_, sub| !sub.hide);
1049 if visible.flatten_help {
1055 for sub in visible.subcommands.values_mut() {
1056 *sub = without_hidden(sub, long);
1057 }
1058 }
1059 visible
1060}
1061
1062static TERA: LazyLock<Tera> = LazyLock::new(|| {
1063 let mut tera = Tera::default();
1064
1065 tera.register_filter(
1067 "ljust",
1068 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1069 let value = value.as_str().unwrap_or("");
1070 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
1071 Ok(format!("{:<width$}", value, width = width))
1072 },
1073 );
1074 tera.register_filter(
1075 "default",
1076 |value: &tera::Value,
1077 kwargs: tera::Kwargs,
1078 _: &tera::State|
1079 -> tera::TeraResult<tera::Value> {
1080 let default_val = kwargs.must_get::<tera::Value>("value")?;
1081 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
1082 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
1083 Ok(default_val)
1084 } else {
1085 Ok(value.clone())
1086 }
1087 },
1088 );
1089 tera.register_filter(
1090 "terminal_wrap",
1091 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1092 let value = value.as_str().unwrap_or("");
1093 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1094 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1095 Ok(crate::docs::layout::render_indented_text(
1096 value, width, indent,
1097 ))
1098 },
1099 );
1100 tera.register_filter(
1101 "terminal_label",
1102 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1103 let value = value.as_str().unwrap_or("");
1104 let label = args.must_get::<String>("label")?;
1105 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1106 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1107 Ok(crate::docs::layout::render_labelled_text(
1108 &label, value, width, indent,
1109 ))
1110 },
1111 );
1112 tera.register_filter(
1113 "terminal_annotation",
1114 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1115 let body = if let Some(values) = value.as_array() {
1116 values
1117 .iter()
1118 .filter_map(tera::Value::as_str)
1119 .collect::<Vec<_>>()
1120 .join(", ")
1121 } else {
1122 value.as_str().unwrap_or("").to_string()
1123 };
1124 let label = args.get::<String>("label")?.unwrap_or_default();
1125 let suffix = args.get::<String>("suffix")?.unwrap_or_default();
1126 let indent = args
1127 .get::<String>("indent")?
1128 .unwrap_or_default()
1129 .chars()
1130 .count();
1131 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1132 let rendered = crate::docs::layout::render_indented_text(
1133 &format!("{label}{body}{suffix}"),
1134 width,
1135 indent,
1136 );
1137 Ok(indent_text(&rendered, indent))
1138 },
1139 );
1140 tera.register_filter(
1141 "terminal_deprecation",
1142 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1143 let field = |name| value.get_from_path(name).and_then(tera::Value::as_str);
1144 let Some(label) = deprecation_label(
1145 field("deprecated"),
1146 field("deprecated_warn_at"),
1147 field("deprecated_remove_at"),
1148 ) else {
1149 return Ok(String::new());
1150 };
1151 let indent = args
1152 .get::<String>("indent")?
1153 .unwrap_or_default()
1154 .chars()
1155 .count();
1156 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1157 let rendered = crate::docs::layout::render_indented_text(&label, width, indent);
1158 Ok(indent_text(&rendered, indent))
1159 },
1160 );
1161
1162 #[rustfmt::skip]
1163 tera.add_raw_templates([
1164 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
1165 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
1166 ]).unwrap();
1167
1168 tera
1169});
1170
1171#[cfg(test)]
1172mod tests {
1173 use super::*;
1174 use insta::assert_snapshot;
1175
1176 #[test]
1177 fn flag_aliases_do_not_leak_into_interactive_help() {
1178 let spec = crate::spec! { r#"
1179bin "ex"
1180flag "-t -f --tail --follow" help="Follow output"
1181 "# }
1182 .unwrap();
1183
1184 for long in [false, true] {
1185 let page = super::render_help(&spec, &spec.cmd, long);
1186 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1187 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1188 }
1189 }
1190
1191 #[test]
1192 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1193 let spec = crate::spec! { r#"
1198bin "ex"
1199flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1200cmd inner help="a command" {
1201 flag "--plain" help="its own"
1202}
1203 "# }
1204 .unwrap();
1205
1206 let inner = spec.cmd.subcommands.get("inner").expect("inner");
1207 for long in [false, true] {
1208 let page = super::render_help(&spec, inner, long);
1209 assert!(
1210 !page.contains("--help"),
1211 "long={long}: a hidden ancestor binds this:\n{page}"
1212 );
1213 assert!(page.contains("-h"), "long={long}:\n{page}");
1215 }
1216 }
1217
1218 #[test]
1219 fn a_long_beats_a_negation_however_far_away_it_is() {
1220 let spec = crate::spec! { r#"
1226bin "ex"
1227flag "--no-cache" global=#true help="the root's plain long"
1228flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1229cmd narrow help="a command" {
1230 flag "--cache" negate="--no-cache" help="its own, with a negation"
1231 flag "--tint" negate="--no-colour" help="claims the root's negation"
1232}
1233 "# }
1234 .unwrap();
1235
1236 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1237 for long in [false, true] {
1238 let page = super::render_help(&spec, narrow, long);
1239 assert!(
1240 page.contains("--no-cache"),
1241 "long={long}: a long beats a negation, so this still binds here:\n{page}"
1242 );
1243 assert!(page.contains("--colour"), "long={long}:\n{page}");
1246 let global = page
1247 .split_once("Global flags:")
1248 .expect("a global section")
1249 .1;
1250 assert!(
1251 !global.contains("--colour / --no-colour"),
1252 "long={long}: the nearer negation owns that spelling:\n{page}"
1253 );
1254 }
1255 }
1256
1257 #[test]
1258 fn a_description_of_only_spaces_is_no_description() {
1259 let spec = crate::spec! { r#"
1266bin "ex"
1267flag "--blank" help=" "
1268flag "--plain" help="plain"
1269 "# }
1270 .unwrap();
1271
1272 for long in [false, true] {
1273 let page = super::render_help(&spec, &spec.cmd, long);
1274 let listing = page.split_once("\nFlags:").expect("a flags section").1;
1277 let line = listing
1278 .lines()
1279 .find(|l| l.contains("--blank"))
1280 .unwrap_or_else(|| panic!("long={long}: {page}"));
1281 assert_eq!(
1282 line,
1283 line.trim_end(),
1284 "long={long}: trailing space on {line:?}"
1285 );
1286 }
1287 }
1288
1289 #[test]
1290 fn test_render_help_omits_hidden_entries() {
1291 let spec = crate::spec! { r#"
1292bin "ex"
1293flag "--visible" help="shown"
1294flag "--secret" hide=#true help="hidden"
1295flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1296arg "[SHOWN]" help="an arg"
1297arg "[HIDDEN]" hide=#true help="a hidden arg"
1298cmd open help="a command"
1299cmd sneaky hide=#true help="a hidden command"
1300 "# }
1301 .unwrap();
1302
1303 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1308 Usage: ex [--visible] [SHOWN] [SUBCOMMAND]
1309
1310 Commands:
1311 open a command
1312 help Print this message or the help of the given subcommand(s)
1313
1314 Arguments:
1315 [SHOWN] an arg
1316
1317 Flags:
1318 --visible shown
1319 -h, --help Print help
1320 ");
1321 }
1322
1323 #[test]
1324 fn test_render_help_groups_by_heading() {
1325 let spec = crate::spec! { r#"
1326bin "testcli"
1327flag "--verbose" help="Verbose output"
1328flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1329flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1330flag "--jobs <n>" help="How many at once" help_heading="Performance"
1331arg "<file>" help="The file"
1332arg "<mode>" help="How to run" help_heading="Behaviour"
1333 "# }
1334 .unwrap();
1335
1336 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1339 Usage: testcli [FLAGS] <file> <mode>
1340
1341 Arguments:
1342 <file> The file
1343
1344 Behaviour:
1345 <mode> How to run
1346
1347 Flags:
1348 --verbose Verbose output
1349 -h, --help Print help
1350
1351 Filtering:
1352 --filter <pattern> Only matching
1353 --exclude <pattern> Skip matching
1354
1355 Performance:
1356 --jobs <n> How many at once
1357 ");
1358 }
1359
1360 #[test]
1361 fn test_render_help_with_only_headed_flags() {
1362 let spec = crate::spec! { r#"
1365bin "testcli"
1366flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1367 "# }
1368 .unwrap();
1369
1370 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1371 Usage: testcli [--filter <pattern>]
1372
1373 Flags:
1374 -h, --help Print help
1375
1376 Filtering:
1377 --filter <pattern> Only matching
1378 ");
1379 }
1380
1381 #[test]
1382 fn test_render_help_with_env() {
1383 let spec = crate::spec! { r#"
1384bin "testcli"
1385flag "--color" env="MYCLI_COLOR" help="Enable color output"
1386flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1387flag "--debug" help="Debug mode"
1388 "# }
1389 .unwrap();
1390
1391 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1392 Usage: testcli [FLAGS]
1393
1394 Flags:
1395 --color Enable color output [env: MYCLI_COLOR]
1396 --verbose Verbose output [env: MYCLI_VERBOSE]
1397 --debug Debug mode
1398 -h, --help Print help
1399 ");
1400
1401 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1402 Usage: testcli [FLAGS]
1403
1404 Flags:
1405 --color Enable color output
1406 [env: MYCLI_COLOR]
1407 --verbose Verbose output
1408 [env: MYCLI_VERBOSE]
1409 --debug Debug mode
1410 -h, --help Print help
1411 ");
1412 }
1413
1414 #[test]
1415 fn test_render_help_with_arg_env() {
1416 let spec = crate::spec! { r#"
1417bin "testcli"
1418arg "<input>" env="MY_INPUT" help="Input file"
1419arg "<output>" env="MY_OUTPUT" help="Output file"
1420arg "<extra>" help="Extra arg without env"
1421arg "[default]" help="Arg with default value" default="default value"
1422 "# }
1423 .unwrap();
1424
1425 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1426 Usage: testcli <ARGS>…
1427
1428 Arguments:
1429 <input> Input file [env: MY_INPUT]
1430 <output> Output file [env: MY_OUTPUT]
1431 <extra> Extra arg without env
1432 [default] Arg with default value (default: default value)
1433
1434 Flags:
1435 -h, --help Print help
1436 ");
1437
1438 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1439 Usage: testcli <ARGS>…
1440
1441 Arguments:
1442 <input> Input file
1443 [env: MY_INPUT]
1444 <output> Output file
1445 [env: MY_OUTPUT]
1446 <extra> Extra arg without env
1447 [default] Arg with default value
1448 (default: default value)
1449
1450 Flags:
1451 -h, --help Print help
1452 ");
1453 }
1454
1455 #[test]
1456 fn test_render_help_with_negated_flag() {
1457 let spec = crate::spec! { r#"
1458bin "testcli"
1459flag "--compress" negate="--no-compress" default=#true help="Compress output"
1460flag "--verbose" help="Verbose output"
1461 "# }
1462 .unwrap();
1463
1464 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1465 Usage: testcli [--compress] [--verbose]
1466
1467 Flags:
1468 --compress / --no-compress Compress output (default: true)
1469 --verbose Verbose output
1470 -h, --help Print help
1471 ");
1472
1473 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1474 Usage: testcli [--compress] [--verbose]
1475
1476 Flags:
1477 --compress / --no-compress Compress output
1478 (default: true)
1479 --verbose Verbose output
1480 -h, --help Print help
1481 ");
1482 }
1483
1484 #[test]
1485 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1486 let spec = crate::spec! { r#"
1487bin "testcli"
1488flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1489 choices {
1490 choice "fast"
1491 choice "slow"
1492 }
1493}
1494flag "--short-only" help="short" hide_long_help=#true
1495flag "--long-only" help="long" hide_short_help=#true
1496arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1497 "# }
1498 .unwrap();
1499
1500 let short = render_help(&spec, &spec.cmd, false);
1501 assert!(short.contains("--mode <mode>"), "{short}");
1502 assert!(short.contains("--short-only"), "{short}");
1503 assert!(!short.contains("--long-only"), "{short}");
1504 assert!(
1505 !short.contains("MODE") && !short.contains("fast, slow"),
1506 "{short}"
1507 );
1508 assert!(
1509 !short.contains("default: fast") && !short.contains("default: file"),
1510 "{short}"
1511 );
1512
1513 let long = render_help(&spec, &spec.cmd, true);
1514 assert!(long.contains("--long-only"), "{long}");
1515 assert!(!long.contains("--short-only"), "{long}");
1516 assert!(
1517 !long.contains("MODE") && !long.contains("possible values"),
1518 "{long}"
1519 );
1520
1521 let rendered = spec.to_string();
1522 let reparsed: crate::Spec = rendered.parse().unwrap();
1523 assert!(reparsed.cmd.flags[0].hide_default_value);
1524 assert!(reparsed.cmd.flags[0].hide_env);
1525 assert!(reparsed.cmd.flags[0].hide_possible_values);
1526 }
1527
1528 #[test]
1529 fn a_help_template_reorders_omits_and_wraps_the_sections() {
1530 let spec = crate::spec! { r#"
1534bin "ex"
1535about "An example"
1536help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1537flag "--force" help="Do it anyway"
1538arg "<file>" help="Which file"
1539cmd "run" help="Run it"
1540 "# }
1541 .unwrap();
1542
1543 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1544 An example
1545
1546 Usage: ex [--force] <file> [SUBCOMMAND]
1547
1548 Flags:
1549 --force Do it anyway
1550 -h, --help Print help
1551
1552 Arguments:
1553 <file> Which file
1554
1555 -- ask a person --
1556 ");
1557 }
1558
1559 #[test]
1560 fn a_template_places_the_sections_a_page_actually_has() {
1561 let spec = crate::spec! { r#"
1567bin "ex"
1568version "1.2.3"
1569about "An example"
1570after_help "Read the docs."
1571help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1572flag "--force" help="Do it anyway"
1573cmd "run" help="Run it"
1574 "# }
1575 .unwrap();
1576
1577 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1578 Usage: ex [--force] [SUBCOMMAND]
1579
1580 Flags:
1581 --force Do it anyway
1582 -h, --help Print help
1583 -V, --version Print version
1584
1585 Commands:
1586 run Run it
1587 help Print this message or the help of the given subcommand(s)
1588
1589 Read the docs.
1590
1591 ex 1.2.3
1592 An example
1593 ");
1594 }
1595
1596 #[test]
1597 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1598 let spec = crate::spec! { r#"
1601bin "ex"
1602flatten_help #true
1603help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1604cmd "run" help="Run it" {
1605 flag "--dry-run" help="Only show changes"
1606}
1607 "# }
1608 .unwrap();
1609
1610 let page = render_help(&spec, &spec.cmd, false);
1611 assert!(
1612 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1613 "{page}"
1614 );
1615 assert!(page.contains("--dry-run"), "{page}");
1616 }
1617
1618 #[test]
1619 fn test_render_help_with_before_after_help() {
1620 let spec = crate::spec! { r#"
1621bin "testcli"
1622before_help "This text appears before the help"
1623after_help "This text appears after the help"
1624flag "--verbose" help="Enable verbose output"
1625 "# }
1626 .unwrap();
1627
1628 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1629 This text appears before the help
1630
1631 Usage: testcli [--verbose]
1632
1633 Flags:
1634 --verbose Enable verbose output
1635 -h, --help Print help
1636
1637 This text appears after the help
1638 ");
1639 }
1640
1641 #[test]
1642 fn test_render_help_with_before_after_help_long() {
1643 let spec = crate::spec! { r#"
1644bin "testcli"
1645before_help "short before"
1646before_help_long "This is the long version of before help"
1647after_help "short after"
1648after_help_long "This is the long version of after help"
1649flag "--verbose" help="Enable verbose output"
1650 "# }
1651 .unwrap();
1652
1653 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1654 short before
1655
1656 Usage: testcli [--verbose]
1657
1658 Flags:
1659 --verbose Enable verbose output
1660 -h, --help Print help
1661
1662 short after
1663 ");
1664
1665 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1666 This is the long version of before help
1667
1668 Usage: testcli [--verbose]
1669
1670 Flags:
1671 --verbose Enable verbose output
1672 -h, --help Print help
1673
1674 This is the long version of after help
1675 ");
1676 }
1677
1678 #[test]
1679 fn root_help_stays_on_the_root_page() {
1680 let spec = crate::spec! { r#"
1681bin "testcli"
1682author "Root Author"
1683license "MIT"
1684before_help "root before"
1685before_help_long "root before long"
1686after_help "root after"
1687after_help_long "root after long"
1688example "testcli --verbose"
1689cmd "run" help="run it" {
1690 after_help "run after"
1691 after_long_help "run after long"
1692 example "testcli run"
1693 cmd "now" help="run it now"
1694}
1695 "# }
1696 .unwrap();
1697 let run = spec.cmd.subcommands.get("run").expect("run command");
1698 let now = run.subcommands.get("now").expect("now command");
1699
1700 for long in [false, true] {
1701 let page = render_help(&spec, now, long);
1702 for root_only in [
1703 "root before",
1704 "root after",
1705 "$ testcli --verbose",
1706 "run after",
1707 "$ testcli run",
1708 "Root Author",
1709 "License: MIT",
1710 ] {
1711 assert!(
1712 !page.contains(root_only),
1713 "long={long}, inherited ancestor metadata {root_only:?}:\n{page}"
1714 );
1715 }
1716 }
1717
1718 let short = render_help(&spec, run, false);
1719 let long = render_help(&spec, run, true);
1720 assert!(short.contains("run after"), "{short}");
1721 assert!(short.contains("$ testcli run"), "{short}");
1722 assert!(long.contains("run after long"), "{long}");
1723 assert!(long.contains("$ testcli run"), "{long}");
1724 }
1725
1726 #[test]
1727 fn test_render_help_with_examples() {
1728 let spec = crate::spec! { r#"
1729bin "testcli"
1730flag "--verbose" help="Enable verbose output"
1731example "testcli --verbose" header="Run with verbose output"
1732example "testcli" header="Run normally" help="Just runs the tool"
1733 "# }
1734 .unwrap();
1735
1736 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1737 Usage: testcli [--verbose]
1738
1739 Flags:
1740 --verbose Enable verbose output
1741 -h, --help Print help
1742
1743 Examples:
1744 Run with verbose output:
1745 $ testcli --verbose
1746 Run normally:
1747 $ testcli
1748 ");
1749
1750 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1751 Usage: testcli [--verbose]
1752
1753 Flags:
1754 --verbose Enable verbose output
1755 -h, --help Print help
1756
1757 Examples:
1758 Run with verbose output:
1759 $ testcli --verbose
1760 Run normally:
1761 Just runs the tool
1762 $ testcli
1763 ");
1764 }
1765
1766 #[test]
1767 fn test_render_help_with_version() {
1768 let spec = crate::spec! { r#"
1769bin "testcli"
1770name "TestCLI"
1771version "1.2.3"
1772flag "--verbose" help="Enable verbose output"
1773 "# }
1774 .unwrap();
1775
1776 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1777 TestCLI 1.2.3
1778 Usage: testcli [--verbose]
1779
1780 Flags:
1781 --verbose Enable verbose output
1782 -h, --help Print help
1783 -V, --version Print version
1784 ");
1785 }
1786
1787 #[test]
1788 fn test_render_help_with_only_long_version() {
1789 let spec = crate::spec! { r#"
1790bin "testcli"
1791long_version "1.2.3\ncommit abc123"
1792flag "--verbose" help="Enable verbose output"
1793 "# }
1794 .unwrap();
1795
1796 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1797 Usage: testcli [--verbose]
1798
1799 Flags:
1800 --verbose Enable verbose output
1801 -h, --help Print help
1802 -V, --version Print version
1803 ");
1804 }
1805
1806 #[test]
1807 fn test_render_help_omits_help_when_disabled() {
1808 let spec = crate::spec! { r#"
1812bin "testcli"
1813version "1.2.3"
1814disable_help #true
1815flag "--verbose" help="Enable verbose output"
1816 "# }
1817 .unwrap();
1818
1819 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1820 testcli 1.2.3
1821 Usage: testcli [--verbose]
1822
1823 Flags:
1824 --verbose Enable verbose output
1825 -V, --version Print version
1826 ");
1827 }
1828
1829 #[test]
1830 fn test_render_help_with_author_license() {
1831 let spec = crate::spec! { r#"
1832bin "testcli"
1833author "Test Author"
1834license "MIT"
1835flag "--verbose" help="Enable verbose output"
1836 "# }
1837 .unwrap();
1838
1839 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1841 Usage: testcli [--verbose]
1842
1843 Flags:
1844 --verbose Enable verbose output
1845 -h, --help Print help
1846 ");
1847
1848 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1850 Usage: testcli [--verbose]
1851
1852 Flags:
1853 --verbose Enable verbose output
1854 -h, --help Print help
1855
1856 Author: Test Author
1857 License: MIT
1858 ");
1859 }
1860
1861 #[test]
1862 fn test_render_help_with_deprecated_command() {
1863 let spec = crate::spec! { r#"
1864bin "testcli"
1865flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
1866cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
1867cmd "new-cmd" help="Do something better"
1868 "# }
1869 .unwrap();
1870
1871 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1872 Usage: testcli [--old] [SUBCOMMAND]
1873
1874 Commands:
1875 new-cmd Do something better
1876 old-cmd Do something [deprecated: use new-cmd instead; warns at 6.2; removed
1877 at 7.0]
1878 help Print this message or the help of the given subcommand(s)
1879
1880 Flags:
1881 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
1882 -h, --help Print help
1883 ");
1884 }
1885
1886 #[test]
1887 fn deprecation_milestones_do_not_need_a_message() {
1888 let spec = crate::spec! { r#"
1889bin "testcli"
1890flag "--old" help="Old switch" deprecated_remove_at="7.0"
1891cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
1892 "# }
1893 .unwrap();
1894
1895 let page = render_help(&spec, &spec.cmd, false);
1896 assert!(
1897 page.contains("old-cmd Do something [deprecated: warns at 6.2]"),
1898 "{page}"
1899 );
1900 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
1901 assert!(!page.contains("[deprecated:;"), "{page}");
1902 }
1903
1904 #[test]
1905 fn test_render_help_with_subcommand_presentation() {
1906 let spec = crate::spec! { r#"
1907bin "testcli"
1908subcommand_help_heading "Actions"
1909subcommand_value_name "ACTION"
1910cmd "run" help="Run it\n"
1911 "# }
1912 .unwrap();
1913
1914 let page = render_help(&spec, &spec.cmd, false);
1915 assert!(page.contains("Usage: testcli [ACTION]"), "{page}");
1916 assert!(page.contains("\nActions:\n"), "{page}");
1917 }
1918
1919 #[test]
1920 fn test_render_help_honors_explicit_display_order() {
1921 let spec = crate::spec! { r#"
1922bin "testcli"
1923flag "--unset" help="Unordered"
1924flag "--later" help="Later" display_order=20
1925flag "--first" help="First" display_order=10
1926cmd "zulu" help="Unordered"
1927cmd "later" help="Later" display_order=20
1928cmd "first" help="First" display_order=10
1929cmd "alpha" help="Unordered"
1930 "# }
1931 .unwrap();
1932
1933 let page = render_help(&spec, &spec.cmd, false);
1934 let commands = page.split_once("\nCommands:\n").unwrap().1;
1935 assert!(
1936 commands.find("first").unwrap() < commands.find("later").unwrap()
1937 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
1938 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
1939 "{page}"
1940 );
1941 let flags = page.split_once("\nFlags:\n").unwrap().1;
1942 assert!(
1943 flags.find("--first").unwrap() < flags.find("--later").unwrap()
1944 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
1945 "{page}"
1946 );
1947 }
1948
1949 #[test]
1950 fn test_render_help_groups_subcommands_by_heading() {
1951 let spec = crate::spec! { r#"
1952bin "testcli"
1953cmd "run" help="Run it" help_heading="Core commands"
1954cmd "clean" help="Remove old state" help_heading="Maintenance"
1955cmd "status" help="Show status" help_heading="Commands"
1956 "# }
1957 .unwrap();
1958
1959 for page in [
1960 render_help(&spec, &spec.cmd, false),
1961 render_help(&spec, &spec.cmd, true),
1962 ] {
1963 let commands = page.find("\nCommands:\n").expect("default command section");
1964 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
1965 let core = page.find("\nCore commands:\n").expect("core section");
1966 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
1967 assert!(commands < core && commands < maintenance, "{page}");
1968 let default_end = core.min(maintenance);
1969 assert!(page[commands..default_end].contains("status"), "{page}");
1970 assert!(page[commands..default_end].contains("help"), "{page}");
1971 assert!(page[core..].contains("run"), "{page}");
1972 assert!(page[maintenance..].contains("clean"), "{page}");
1973 }
1974 }
1975
1976 #[test]
1977 fn test_render_help_with_next_line_layout() {
1978 let spec = crate::spec! { r#"
1979bin "testcli"
1980next_line_help #true
1981arg "<input>" help="Input file" env="INPUT" default="fast" {
1982 choices {
1983 choice "fast"
1984 choice "slow"
1985 }
1986}
1987flag "--verbose" help="Enable verbose output"
1988cmd "run" help="Run it"
1989 "# }
1990 .unwrap();
1991
1992 let short = render_help(&spec, &spec.cmd, false);
1993 assert!(!short.contains(" Run it\n\n help"), "{short}");
1994 for page in [short, render_help(&spec, &spec.cmd, true)] {
1995 assert!(page.contains(" [input]\n Input file"), "{page}");
1996 assert!(
1997 page.contains("--verbose\n Enable verbose output"),
1998 "{page}"
1999 );
2000 assert!(
2001 page.contains(
2002 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
2003 ),
2004 "{page}"
2005 );
2006 assert!(page.contains(" run\n Run it"), "{page}");
2007 }
2008 }
2009
2010 #[test]
2011 fn flatten_help_expands_subcommands_instead_of_listing_them() {
2012 let spec = crate::spec! { r#"
2013bin "testcli"
2014flatten_help #true
2015next_line_help #true
2016cmd "run" help="Run it" {
2017 arg "<task>" help="Task name" env="TASK" default="build" {
2018 choices {
2019 choice "build"
2020 choice "test"
2021 }
2022 }
2023 flag "--dry-run" help="Only show changes"
2024 flatten_help #true
2025 cmd "nested" help="Nested operation" {
2026 flag "--deep" help="Deep option"
2027 }
2028}
2029 "# }
2030 .unwrap();
2031
2032 for page in [
2033 render_help(&spec, &spec.cmd, false),
2034 render_help(&spec, &spec.cmd, true),
2035 ] {
2036 assert!(
2037 page.contains("Usage: testcli\n testcli run"),
2038 "{page}"
2039 );
2040 assert!(!page.contains("\nCommands:\n"), "{page}");
2041 assert!(page.contains("\nrun:\nRun it"), "{page}");
2042 assert!(page.contains("[task]"), "{page}");
2043 assert!(page.contains("--dry-run"), "{page}");
2044 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
2045 assert!(page.contains("--deep"), "{page}");
2046 assert!(
2047 page.contains(
2048 " [possible values: build, test]\n [env: TASK]\n (default: build)"
2049 ),
2050 "{page}"
2051 );
2052 }
2053 }
2054
2055 #[test]
2056 fn styled_help_colours_semantics_and_template_styles() {
2057 let mut spec = crate::spec! { r#"
2058bin "testcli"
2059help_template "{$bright-blue}My tool{/$}\n\n{{usage}}\n\n{{commands}}\n\n{{flags}}\n\n{{after_help}}"
2060cmd "build" help="Build it"
2061flag "--output <FILE>" help="Write **the file**"
2062 "# }
2063 .unwrap();
2064 spec.cmd.after_help_long = Some(
2065 "\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n \u{1b}[1mtestcli run\u{1b}[22m"
2066 .to_string(),
2067 );
2068
2069 let plain = render_help(&spec, &spec.cmd, true);
2070 assert!(plain.contains("My tool"), "{plain}");
2071 assert!(plain.contains("Examples:\n\n testcli run"), "{plain}");
2072 assert!(!plain.contains('\u{1b}'), "{plain:?}");
2073
2074 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2075 assert!(
2076 coloured.contains("\u{1b}[94mMy tool\u{1b}[0m"),
2077 "{coloured:?}"
2078 );
2079 assert!(
2080 coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"),
2081 "{coloured:?}"
2082 );
2083 assert!(
2084 coloured.contains("\u{1b}[1;32m--output\u{1b}[0m"),
2085 "{coloured:?}"
2086 );
2087 assert!(
2088 coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"),
2089 "{coloured:?}"
2090 );
2091 assert!(
2092 coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
2093 "{coloured:?}"
2094 );
2095 assert!(
2096 coloured.contains(
2097 "\u{1b}[1;32mhelp\u{1b}[0m Print this message or the help of the given subcommand(s)"
2098 ),
2099 "{coloured:?}"
2100 );
2101 assert!(
2102 coloured.contains("\u{1b}[1mthe file\u{1b}[22m"),
2103 "{coloured:?}"
2104 );
2105 assert!(
2106 coloured.contains("\u{1b}[1m\u{1b}[4mExamples:"),
2107 "{coloured:?}"
2108 );
2109 }
2110
2111 #[test]
2112 fn command_style_does_not_match_command_group_prose() {
2113 let spec = crate::spec! { r#"
2114bin "testcli"
2115heading "Build commands" help="build these projects before publishing"
2116cmd "build" help="Build it" help_heading="Build commands"
2117 "# }
2118 .unwrap();
2119
2120 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2121 assert!(
2122 coloured.contains(" build these projects before publishing"),
2123 "{coloured:?}"
2124 );
2125 assert!(
2126 coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
2127 "{coloured:?}"
2128 );
2129 assert!(
2130 !coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m these projects"),
2131 "{coloured:?}"
2132 );
2133 }
2134
2135 #[test]
2136 fn flattened_help_does_not_apply_command_style() {
2137 let spec = crate::spec! { r#"
2138bin "testcli"
2139flatten_help #true
2140help_template "Intro\n run should stay prose\n\n{{commands}}"
2141cmd "run" help="Run it"
2142 "# }
2143 .unwrap();
2144
2145 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2146 assert!(
2147 coloured.contains(" run should stay prose"),
2148 "{coloured:?}"
2149 );
2150 assert!(
2151 !coloured.contains("\u{1b}[1;32mrun\u{1b}[0m"),
2152 "{coloured:?}"
2153 );
2154 }
2155}