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 page.trim().to_string() + "\n"
207}
208
209const MARKS: [(&str, &str); 6] = [
220 ("mark_usage", "\u{1}usage\u{1}"),
221 ("mark_commands", "\u{1}commands\u{1}"),
222 ("mark_args", "\u{1}args\u{1}"),
223 ("mark_flags", "\u{1}flags\u{1}"),
224 ("mark_flattened", "\u{1}flattened\u{1}"),
225 ("mark_after_help", "\u{1}after_help\u{1}"),
226];
227const MARK_GROUPED_ARGS: &str = "\u{1}grouped_args\u{1}";
228const MARK_GROUPED_FLAGS: &str = "\u{1}grouped_flags\u{1}";
229const MARK_GLOBAL_FLAGS: &str = "\u{1}global_flags\u{1}";
230
231struct Sections<'a> {
237 about: &'a str,
238 usage: &'a str,
239 commands: &'a str,
240 args: String,
241 flags: String,
242 grouped_args: &'a str,
243 ungrouped_args: &'a str,
244 grouped_flags: &'a str,
245 ungrouped_flags: String,
246 flattened: &'a str,
247 after_help: &'a str,
248}
249
250impl<'a> Sections<'a> {
251 fn split(rendered: &'a str) -> Self {
252 let mut rest = rendered;
253 let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
254 for (_, mark) in MARKS {
255 match rest.split_once(mark) {
259 Some((before, after)) => {
260 parts.push(before);
261 rest = after;
262 }
263 None => parts.push(""),
264 }
265 }
266 parts.push(rest);
267 let (ungrouped_args, grouped_args) = parts[3]
268 .split_once(MARK_GROUPED_ARGS)
269 .unwrap_or((parts[3], ""));
270 let (own_flags, global_flags) = parts[4]
271 .split_once(MARK_GLOBAL_FLAGS)
272 .unwrap_or((parts[4], ""));
273 let (own_ungrouped_flags, grouped_flags) = own_flags
274 .split_once(MARK_GROUPED_FLAGS)
275 .unwrap_or((own_flags, ""));
276 Self {
277 about: parts[0],
278 usage: parts[1],
279 commands: parts[2],
280 args: format!("{ungrouped_args}{grouped_args}"),
281 flags: format!("{own_ungrouped_flags}{grouped_flags}{global_flags}"),
282 grouped_args,
283 ungrouped_args,
284 grouped_flags,
285 ungrouped_flags: format!("{own_ungrouped_flags}{global_flags}"),
286 flattened: parts[5],
287 after_help: parts[6],
288 }
289 }
290
291 fn concatenated(&self) -> String {
293 [
294 self.about,
295 self.usage,
296 self.commands,
297 self.args.as_str(),
298 self.flags.as_str(),
299 self.flattened,
300 self.after_help,
301 ]
302 .concat()
303 }
304
305 fn named(&self, name: &str) -> Option<String> {
307 Some(match name {
308 "about" => self.about.trim().to_string(),
309 "usage" => self.usage.trim().to_string(),
310 "commands" => {
311 let mut out = self.commands.trim().to_string();
312 let flattened = self.flattened.trim();
313 if !flattened.is_empty() {
314 if !out.is_empty() {
315 out.push_str("\n\n");
316 }
317 out.push_str(flattened);
318 }
319 out
320 }
321 "args" => self.args.trim().to_string(),
322 "flags" => self.flags.trim().to_string(),
323 "grouped_args" => self.grouped_args.trim().to_string(),
324 "ungrouped_args" => self.ungrouped_args.trim().to_string(),
325 "grouped_flags" => self.grouped_flags.trim().to_string(),
326 "ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
327 "after_help" => self.after_help.trim().to_string(),
328 _ => return None,
329 })
330 }
331}
332
333fn supplied_flags(
347 spec: &Spec,
348 cmd: &SpecCommand,
349 ancestors_taken: &[String],
350 is_root: bool,
351) -> Vec<crate::docs::models::SpecFlag> {
352 let mut taken: Vec<String> = ancestors_taken.to_vec();
357 for f in &cmd.flags {
358 taken.extend(f.long.iter().map(|l| format!("--{l}")));
359 taken.extend(f.short.iter().map(|s| format!("-{s}")));
360 taken.extend(f.negate.clone());
362 }
363
364 let build = |name: &str, long: &str, short: char, help: &str| {
365 let long_free = !taken.contains(&format!("--{long}"));
366 let short_free = !taken.contains(&format!("-{short}"));
367 if !long_free && !short_free {
368 return None;
369 }
370 let name = if long_free { name } else { &short.to_string() };
373 let mut flag = crate::SpecFlag {
374 name: name.to_string(),
375 long: if long_free {
376 vec![long.to_string()]
377 } else {
378 vec![]
379 },
380 short: if short_free { vec![short] } else { vec![] },
381 help: Some(help.to_string()),
382 ..Default::default()
383 };
384 flag.usage = flag.usage();
385 Some(crate::docs::models::SpecFlag::from(&flag))
386 };
387
388 let mut out = Vec::new();
389 if spec.disable_help != Some(true) && !cmd.disable_help_flag {
396 out.extend(build("help", "help", 'h', "Print help"));
397 }
398 if is_root
399 && (spec.version.is_some() || spec.long_version.is_some())
400 && !cmd.disable_version_flag
401 {
402 out.extend(build("version", "version", 'V', "Print version"));
403 }
404 out
405}
406
407fn lay_out(flags: &mut [crate::docs::models::SpecFlag], column: Column) {
414 for flag in flags {
415 flag.usage_col_width = column.col;
416 flag.help_is_block =
417 !column.can_inline(crate::docs::layout::visible_width(&flag.display_usage));
418 let text = if column.long {
419 flag.help_long
420 .as_deref()
421 .or(flag.help.as_deref())
422 .map(str::to_string)
423 } else if column.next_line {
424 flag.help.clone()
425 } else {
426 with_annotations(flag.help.as_deref(), flag_annotations(flag))
427 };
428 wrap_into(
429 text,
430 column,
431 crate::docs::layout::visible_width(&flag.display_usage),
432 &mut flag.row,
433 &mut flag.help_rendered,
434 &mut flag.help_is_multiline,
435 &mut flag.ann_indent,
436 );
437 }
438}
439
440fn lay_out_args(args: &mut [crate::docs::models::SpecArg], column: Column) {
446 for arg in args {
447 arg.usage_col_width = column.col;
448 arg.help_is_block = !column.can_inline(crate::docs::layout::visible_width(&arg.usage));
449 let text = if column.long {
450 arg.help_long
451 .as_deref()
452 .or(arg.help.as_deref())
453 .map(str::to_string)
454 } else if column.next_line {
455 arg.help.clone()
456 } else {
457 with_annotations(arg.help.as_deref(), arg_annotations(arg))
458 };
459 wrap_into(
460 text,
461 column,
462 crate::docs::layout::visible_width(&arg.usage),
463 &mut arg.row,
464 &mut arg.help_rendered,
465 &mut arg.help_is_multiline,
466 &mut arg.ann_indent,
467 );
468 }
469}
470
471fn lay_out_flattened(commands: &mut [crate::docs::models::SpecCommand], width: usize, long: bool) {
472 for command in commands {
473 lay_out_group_help(&mut command.arg_groups, width);
474 lay_out_group_help(&mut command.flag_groups, width);
475 let arg_col = crate::docs::layout::usage_column_width(
476 command
477 .arg_groups
478 .iter()
479 .flat_map(|group| group.items.iter())
480 .map(|arg| arg.usage.as_str()),
481 width,
482 );
483 let arg_column = Column {
484 width,
485 col: arg_col,
486 long,
487 next_line: command.flattened_next_line_help,
488 };
489 for group in &mut command.arg_groups {
490 lay_out_args(&mut group.items, arg_column);
491 }
492
493 let flag_col = crate::docs::layout::usage_column_width(
494 command
495 .flag_groups
496 .iter()
497 .flat_map(|group| group.items.iter())
498 .map(|flag| flag.display_usage.as_str()),
499 width,
500 );
501 let flag_column = Column {
502 width,
503 col: flag_col,
504 long,
505 next_line: command.flattened_next_line_help,
506 };
507 for group in &mut command.flag_groups {
508 lay_out(&mut group.items, flag_column);
509 }
510 }
511}
512
513fn lay_out_group_help<T>(groups: &mut [crate::docs::models::Group<T>], width: usize) {
514 for group in groups {
515 group.help_rendered = group
516 .help
517 .as_deref()
518 .map(|help| crate::docs::layout::render_indented_text(help, width, 2));
519 }
520}
521
522fn wrap_into(
528 text: Option<String>,
529 column: Column,
530 usage_width: usize,
531 row: &mut Option<String>,
532 help_rendered: &mut Option<String>,
533 help_is_multiline: &mut bool,
534 ann_indent: &mut String,
535) {
536 *row = None;
537 *help_rendered = None;
538 *help_is_multiline = false;
539 *ann_indent = column.annotation_indent(column.can_inline(usage_width));
542 let Some(text) = text else { return };
543 if !column.can_inline(usage_width) {
544 let indent = column.block_indent();
545 *row = Some(indent_text(
546 &crate::docs::layout::render_indented_text(&text, column.width, indent),
547 indent,
548 ));
549 return;
550 }
551 let first_indent = column.inline_help_start(usage_width);
552 let continuation_indent = column.description_indent();
553 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
554 &text,
555 column.width,
556 first_indent,
557 continuation_indent,
558 );
559 *ann_indent = column.annotation_indent(!rendered.is_empty());
563 if !rendered.is_empty() {
564 *help_rendered = Some(rendered);
565 *help_is_multiline = is_multiline;
566 }
567 *row = Some(text);
568}
569
570fn indent_text(text: &str, indent: usize) -> String {
571 let pad = " ".repeat(indent);
572 text.lines()
573 .map(|line| {
574 if line.is_empty() {
575 String::new()
576 } else {
577 format!("{pad}{line}")
578 }
579 })
580 .collect::<Vec<_>>()
581 .join("\n")
582}
583
584fn with_annotations(help: Option<&str>, annotations: Vec<String>) -> Option<String> {
591 let mut parts = Vec::new();
592 if let Some(help) = summarize(help) {
593 parts.push(help.to_string());
594 }
595 parts.extend(annotations);
596 (!parts.is_empty()).then(|| parts.join(" "))
597}
598
599fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec<String> {
601 let mut parts = value_annotations(
602 flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()),
603 flag.hide_possible_values,
604 flag.env.as_deref(),
605 flag.hide_env,
606 &flag.env_fallback,
607 &flag.deprecated_env,
608 &flag.default,
609 flag.hide_default_value,
610 );
611 if let Some(label) = deprecation_label(
612 flag.deprecated.as_deref(),
613 flag.deprecated_warn_at.as_deref(),
614 flag.deprecated_remove_at.as_deref(),
615 ) {
616 parts.push(label);
617 }
618 parts
619}
620
621fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec<String> {
623 value_annotations(
624 arg.choices.as_ref(),
625 arg.hide_possible_values,
626 arg.env.as_deref(),
627 arg.hide_env,
628 &arg.env_fallback,
629 &arg.deprecated_env,
630 &arg.default,
631 arg.hide_default_value,
632 )
633}
634
635#[allow(clippy::too_many_arguments)]
637fn value_annotations(
638 choices: Option<&crate::SpecChoices>,
639 hide_possible_values: bool,
640 env: Option<&str>,
641 hide_env: bool,
642 env_fallback: &[String],
643 deprecated_env: &[String],
644 default: &[String],
645 hide_default_value: bool,
646) -> Vec<String> {
647 let mut parts = Vec::new();
648 if let Some(choices) = choices.filter(|_| !hide_possible_values) {
649 if !choices.choices.is_empty() {
650 parts.push(format!("[{}]", choices.choices.join(", ")));
651 }
652 if let Some(env) = choices.env() {
653 parts.push(format!("[choices env: {env}]"));
654 }
655 }
656 if !hide_env {
657 if let Some(env) = env {
658 parts.push(format!("[env: {env}]"));
659 }
660 parts.extend(
661 env_fallback
662 .iter()
663 .map(|env| format!("[env fallback: {env}]")),
664 );
665 parts.extend(
666 deprecated_env
667 .iter()
668 .map(|env| format!("[deprecated env: {env}]")),
669 );
670 }
671 if !hide_default_value && !default.is_empty() {
672 parts.push(format!("(default: {})", default.join(", ")));
673 }
674 parts
675}
676
677#[derive(Clone, Copy)]
679struct Column {
680 width: usize,
682 col: usize,
684 long: bool,
686 next_line: bool,
688}
689
690const BLOCK_INDENT: usize = 4;
692
693impl Column {
694 fn usage_overflows(&self, usage_width: usize) -> bool {
695 !self.next_line && usage_width > self.col
696 }
697
698 fn description_indent(&self) -> usize {
699 2 + self.col + 2
700 }
701
702 fn inline_help_start(&self, usage_width: usize) -> usize {
703 if self.usage_overflows(usage_width) {
704 2 + usage_width + 2
705 } else {
706 self.description_indent()
707 }
708 }
709
710 fn can_inline(&self, usage_width: usize) -> bool {
712 if self.next_line {
713 return false;
714 }
715 let minimum = if self.usage_overflows(usage_width) {
716 crate::docs::layout::MIN_INLINE_HELP_WIDTH
717 } else {
718 10
719 };
720 self.width
721 .saturating_sub(self.inline_help_start(usage_width))
722 >= minimum
723 }
724
725 fn annotation_indent(&self, reached_column: bool) -> String {
732 let indent = if reached_column {
733 self.description_indent()
734 } else {
735 self.block_indent()
736 };
737 " ".repeat(indent)
738 }
739
740 fn block_indent(&self) -> usize {
743 let description = self.description_indent();
744 if !self.next_line && self.width.saturating_sub(description) >= 10 {
745 description
746 } else {
747 BLOCK_INDENT
748 }
749 }
750}
751
752const HELP_SUBCOMMAND: &str = "help";
754const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
755
756fn lay_out_commands(
761 commands: &mut [crate::docs::models::HelpCommand],
762 terminal_width: usize,
763 col: usize,
764 next_line: bool,
765) {
766 let column = Column {
767 width: terminal_width,
768 col,
769 long: false,
770 next_line,
771 };
772 for command in commands {
773 command.usage_col_width = col;
774 command.row = command_row(command);
775 command.help_rendered = None;
776 command.help_is_multiline = false;
777 let usage_width = crate::docs::layout::visible_width(&command.name);
778 if let Some(row) = command.row.as_deref() {
779 if column.can_inline(usage_width) {
780 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
781 row,
782 terminal_width,
783 column.inline_help_start(usage_width),
784 column.description_indent(),
785 );
786 if !rendered.is_empty() {
787 command.help_rendered = Some(rendered);
788 command.help_is_multiline = is_multiline;
789 }
790 } else if let Some(row) = command.row.as_mut() {
791 let indent = column.block_indent();
792 *row = indent_text(
793 &crate::docs::layout::render_indented_text(row, terminal_width, indent),
794 indent,
795 );
796 }
797 }
798 }
799}
800
801fn command_row(cmd: &crate::docs::models::HelpCommand) -> Option<String> {
808 let mut parts = Vec::new();
809 let summary = summarize(cmd.help.as_deref()).or_else(|| {
812 summarize(
813 cmd.help_long
814 .as_deref()
815 .and_then(|help| help.lines().next()),
816 )
817 });
818 if let Some(summary) = summary {
819 parts.push(summary.to_string());
820 }
821 if !cmd.aliases.is_empty() {
822 parts.push(format!("[aliases: {}]", cmd.aliases.join(", ")));
823 }
824 if let Some(label) = deprecation_label(
825 cmd.deprecated.as_deref(),
826 cmd.deprecated_warn_at.as_deref(),
827 cmd.deprecated_remove_at.as_deref(),
828 ) {
829 parts.push(label);
830 }
831 (!parts.is_empty()).then(|| parts.join(" "))
832}
833
834fn summarize(text: Option<&str>) -> Option<&str> {
836 text.map(str::trim_end).filter(|text| !text.is_empty())
837}
838
839fn deprecation_label(
841 message: Option<&str>,
842 warn_at: Option<&str>,
843 remove_at: Option<&str>,
844) -> Option<String> {
845 if message.is_none() && warn_at.is_none() && remove_at.is_none() {
846 return None;
847 }
848 let mut parts = Vec::new();
849 if let Some(message) = message {
850 parts.push(message.to_string());
851 }
852 if let Some(at) = warn_at {
853 parts.push(format!("warns at {at}"));
854 }
855 if let Some(at) = remove_at {
856 parts.push(format!("removed at {at}"));
857 }
858 Some(format!("[deprecated: {}]", parts.join("; ")))
859}
860
861fn render_row(
863 name: &str,
864 row: &str,
865 terminal_width: usize,
866 col: usize,
867 next_line_help: bool,
868) -> String {
869 if !next_line_help {
870 if crate::docs::layout::visible_width(name) <= col {
871 let (rendered, _) = crate::docs::layout::render_help_text(row, terminal_width, col);
872 if !rendered.is_empty() {
873 return format!(" {name:<col$} {rendered}");
874 }
875 } else if !row.contains('\n') {
876 return format!(
877 " {name}\n {}",
878 crate::docs::layout::render_block_text(row, terminal_width)
879 );
880 }
881 }
882 format!(" {name}\n {row}")
883}
884
885fn inherited_flags(
894 spec: &Spec,
895 cmd: &SpecCommand,
896 full_cmd: &[String],
897 long_help: bool,
898) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
899 let mut ancestors: Vec<&SpecCommand> = Vec::new();
901 let mut at = &spec.cmd;
902 for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
903 ancestors.push(at);
904 let Some(next) = at.subcommands.get(name) else {
905 return (Vec::new(), Vec::new());
906 };
907 at = next;
908 }
909 if !full_cmd.is_empty() {
910 ancestors.push(at);
911 }
912
913 let forms = |f: &crate::SpecFlag| -> Vec<String> {
926 f.long
927 .iter()
928 .map(|l| format!("--{l}"))
929 .chain(f.short.iter().map(|s| format!("-{s}")))
930 .collect()
931 };
932 let every_form: Vec<String> = cmd
933 .flags
934 .iter()
935 .chain(
936 ancestors
937 .iter()
938 .flat_map(|a| a.flags.iter())
939 .filter(|f| f.global),
940 )
941 .flat_map(&forms)
942 .collect();
943
944 let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
945 let mut taken_negations: Vec<String> =
946 cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
947 let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
948 for ancestor in ancestors.iter().rev() {
949 for f in ancestor.flags.iter().filter(|f| f.global) {
950 let long = f
951 .long
952 .iter()
953 .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
954 .cloned();
955 let short = f
956 .short
957 .iter()
958 .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
959 .copied();
960 let mine = forms(f);
961 let negate = f.negate.as_ref().is_some_and(|n| {
962 !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
963 });
964 taken.extend(forms(f));
967 taken_negations.extend(f.negate.clone());
968 if f.hide
969 || if long_help {
970 f.hide_long_help
971 } else {
972 f.hide_short_help
973 }
974 || (long.is_none() && short.is_none() && !negate)
975 {
976 continue;
977 }
978 keep.push((f, long, short, negate));
979 }
980 }
981 let shown: Vec<crate::docs::models::SpecFlag> = ancestors
982 .iter()
983 .flat_map(|a| a.flags.iter())
984 .filter_map(|f| {
985 keep.iter()
986 .find(|(k, _, _, _)| std::ptr::eq(*k, f))
987 .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
988 })
989 .map(|(f, long, short, negate)| {
990 let mut shown = f.clone();
993 shown.long = long.into_iter().collect();
994 shown.short = short.into_iter().collect();
995 if !negate {
996 shown.negate = None;
997 }
998 shown.usage = shown.usage();
999 crate::docs::models::SpecFlag::from(&shown)
1000 })
1001 .collect();
1002 taken.extend(taken_negations);
1006 (shown, taken)
1007}
1008
1009fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
1021 let mut visible = cmd.clone();
1022 visible.flags.retain(|flag| {
1023 !flag.hide
1024 && if long {
1025 !flag.hide_long_help
1026 } else {
1027 !flag.hide_short_help
1028 }
1029 });
1030 visible.args.retain(|arg| {
1031 !arg.hide
1032 && if long {
1033 !arg.hide_long_help
1034 } else {
1035 !arg.hide_short_help
1036 }
1037 });
1038 visible.subcommands.retain(|_, sub| !sub.hide);
1039 if visible.flatten_help {
1045 for sub in visible.subcommands.values_mut() {
1046 *sub = without_hidden(sub, long);
1047 }
1048 }
1049 visible
1050}
1051
1052static TERA: LazyLock<Tera> = LazyLock::new(|| {
1053 let mut tera = Tera::default();
1054
1055 tera.register_filter(
1057 "ljust",
1058 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1059 let value = value.as_str().unwrap_or("");
1060 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
1061 Ok(format!("{:<width$}", value, width = width))
1062 },
1063 );
1064 tera.register_filter(
1065 "default",
1066 |value: &tera::Value,
1067 kwargs: tera::Kwargs,
1068 _: &tera::State|
1069 -> tera::TeraResult<tera::Value> {
1070 let default_val = kwargs.must_get::<tera::Value>("value")?;
1071 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
1072 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
1073 Ok(default_val)
1074 } else {
1075 Ok(value.clone())
1076 }
1077 },
1078 );
1079 tera.register_filter(
1080 "terminal_wrap",
1081 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1082 let value = value.as_str().unwrap_or("");
1083 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1084 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1085 Ok(crate::docs::layout::render_indented_text(
1086 value, width, indent,
1087 ))
1088 },
1089 );
1090 tera.register_filter(
1091 "terminal_label",
1092 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1093 let value = value.as_str().unwrap_or("");
1094 let label = args.must_get::<String>("label")?;
1095 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1096 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1097 Ok(crate::docs::layout::render_labelled_text(
1098 &label, value, width, indent,
1099 ))
1100 },
1101 );
1102 tera.register_filter(
1103 "terminal_annotation",
1104 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1105 let body = if let Some(values) = value.as_array() {
1106 values
1107 .iter()
1108 .filter_map(tera::Value::as_str)
1109 .collect::<Vec<_>>()
1110 .join(", ")
1111 } else {
1112 value.as_str().unwrap_or("").to_string()
1113 };
1114 let label = args.get::<String>("label")?.unwrap_or_default();
1115 let suffix = args.get::<String>("suffix")?.unwrap_or_default();
1116 let indent = args
1117 .get::<String>("indent")?
1118 .unwrap_or_default()
1119 .chars()
1120 .count();
1121 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1122 let rendered = crate::docs::layout::render_indented_text(
1123 &format!("{label}{body}{suffix}"),
1124 width,
1125 indent,
1126 );
1127 Ok(indent_text(&rendered, indent))
1128 },
1129 );
1130 tera.register_filter(
1131 "terminal_deprecation",
1132 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1133 let field = |name| value.get_from_path(name).and_then(tera::Value::as_str);
1134 let Some(label) = deprecation_label(
1135 field("deprecated"),
1136 field("deprecated_warn_at"),
1137 field("deprecated_remove_at"),
1138 ) else {
1139 return Ok(String::new());
1140 };
1141 let indent = args
1142 .get::<String>("indent")?
1143 .unwrap_or_default()
1144 .chars()
1145 .count();
1146 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1147 let rendered = crate::docs::layout::render_indented_text(&label, width, indent);
1148 Ok(indent_text(&rendered, indent))
1149 },
1150 );
1151
1152 #[rustfmt::skip]
1153 tera.add_raw_templates([
1154 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
1155 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
1156 ]).unwrap();
1157
1158 tera
1159});
1160
1161#[cfg(test)]
1162mod tests {
1163 use super::*;
1164 use insta::assert_snapshot;
1165
1166 #[test]
1167 fn flag_aliases_do_not_leak_into_interactive_help() {
1168 let spec = crate::spec! { r#"
1169bin "ex"
1170flag "-t -f --tail --follow" help="Follow output"
1171 "# }
1172 .unwrap();
1173
1174 for long in [false, true] {
1175 let page = super::render_help(&spec, &spec.cmd, long);
1176 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1177 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1178 }
1179 }
1180
1181 #[test]
1182 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1183 let spec = crate::spec! { r#"
1188bin "ex"
1189flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1190cmd inner help="a command" {
1191 flag "--plain" help="its own"
1192}
1193 "# }
1194 .unwrap();
1195
1196 let inner = spec.cmd.subcommands.get("inner").expect("inner");
1197 for long in [false, true] {
1198 let page = super::render_help(&spec, inner, long);
1199 assert!(
1200 !page.contains("--help"),
1201 "long={long}: a hidden ancestor binds this:\n{page}"
1202 );
1203 assert!(page.contains("-h"), "long={long}:\n{page}");
1205 }
1206 }
1207
1208 #[test]
1209 fn a_long_beats_a_negation_however_far_away_it_is() {
1210 let spec = crate::spec! { r#"
1216bin "ex"
1217flag "--no-cache" global=#true help="the root's plain long"
1218flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1219cmd narrow help="a command" {
1220 flag "--cache" negate="--no-cache" help="its own, with a negation"
1221 flag "--tint" negate="--no-colour" help="claims the root's negation"
1222}
1223 "# }
1224 .unwrap();
1225
1226 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1227 for long in [false, true] {
1228 let page = super::render_help(&spec, narrow, long);
1229 assert!(
1230 page.contains("--no-cache"),
1231 "long={long}: a long beats a negation, so this still binds here:\n{page}"
1232 );
1233 assert!(page.contains("--colour"), "long={long}:\n{page}");
1236 let global = page
1237 .split_once("Global flags:")
1238 .expect("a global section")
1239 .1;
1240 assert!(
1241 !global.contains("--colour / --no-colour"),
1242 "long={long}: the nearer negation owns that spelling:\n{page}"
1243 );
1244 }
1245 }
1246
1247 #[test]
1248 fn a_description_of_only_spaces_is_no_description() {
1249 let spec = crate::spec! { r#"
1256bin "ex"
1257flag "--blank" help=" "
1258flag "--plain" help="plain"
1259 "# }
1260 .unwrap();
1261
1262 for long in [false, true] {
1263 let page = super::render_help(&spec, &spec.cmd, long);
1264 let listing = page.split_once("\nFlags:").expect("a flags section").1;
1267 let line = listing
1268 .lines()
1269 .find(|l| l.contains("--blank"))
1270 .unwrap_or_else(|| panic!("long={long}: {page}"));
1271 assert_eq!(
1272 line,
1273 line.trim_end(),
1274 "long={long}: trailing space on {line:?}"
1275 );
1276 }
1277 }
1278
1279 #[test]
1280 fn test_render_help_omits_hidden_entries() {
1281 let spec = crate::spec! { r#"
1282bin "ex"
1283flag "--visible" help="shown"
1284flag "--secret" hide=#true help="hidden"
1285flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1286arg "[SHOWN]" help="an arg"
1287arg "[HIDDEN]" hide=#true help="a hidden arg"
1288cmd open help="a command"
1289cmd sneaky hide=#true help="a hidden command"
1290 "# }
1291 .unwrap();
1292
1293 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1298 Usage: ex [--visible] [SHOWN] <SUBCOMMAND>
1299
1300 Commands:
1301 open a command
1302 help Print this message or the help of the given subcommand(s)
1303
1304 Arguments:
1305 [SHOWN] an arg
1306
1307 Flags:
1308 --visible shown
1309 -h, --help Print help
1310 ");
1311 }
1312
1313 #[test]
1314 fn test_render_help_groups_by_heading() {
1315 let spec = crate::spec! { r#"
1316bin "testcli"
1317flag "--verbose" help="Verbose output"
1318flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1319flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1320flag "--jobs <n>" help="How many at once" help_heading="Performance"
1321arg "<file>" help="The file"
1322arg "<mode>" help="How to run" help_heading="Behaviour"
1323 "# }
1324 .unwrap();
1325
1326 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1329 Usage: testcli [FLAGS] <file> <mode>
1330
1331 Arguments:
1332 <file> The file
1333
1334 Behaviour:
1335 <mode> How to run
1336
1337 Flags:
1338 --verbose Verbose output
1339 -h, --help Print help
1340
1341 Filtering:
1342 --filter <pattern> Only matching
1343 --exclude <pattern> Skip matching
1344
1345 Performance:
1346 --jobs <n> How many at once
1347 ");
1348 }
1349
1350 #[test]
1351 fn test_render_help_with_only_headed_flags() {
1352 let spec = crate::spec! { r#"
1355bin "testcli"
1356flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1357 "# }
1358 .unwrap();
1359
1360 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1361 Usage: testcli [--filter <pattern>]
1362
1363 Flags:
1364 -h, --help Print help
1365
1366 Filtering:
1367 --filter <pattern> Only matching
1368 ");
1369 }
1370
1371 #[test]
1372 fn test_render_help_with_env() {
1373 let spec = crate::spec! { r#"
1374bin "testcli"
1375flag "--color" env="MYCLI_COLOR" help="Enable color output"
1376flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1377flag "--debug" help="Debug mode"
1378 "# }
1379 .unwrap();
1380
1381 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1382 Usage: testcli [FLAGS]
1383
1384 Flags:
1385 --color Enable color output [env: MYCLI_COLOR]
1386 --verbose Verbose output [env: MYCLI_VERBOSE]
1387 --debug Debug mode
1388 -h, --help Print help
1389 ");
1390
1391 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1392 Usage: testcli [FLAGS]
1393
1394 Flags:
1395 --color Enable color output
1396 [env: MYCLI_COLOR]
1397 --verbose Verbose output
1398 [env: MYCLI_VERBOSE]
1399 --debug Debug mode
1400 -h, --help Print help
1401 ");
1402 }
1403
1404 #[test]
1405 fn test_render_help_with_arg_env() {
1406 let spec = crate::spec! { r#"
1407bin "testcli"
1408arg "<input>" env="MY_INPUT" help="Input file"
1409arg "<output>" env="MY_OUTPUT" help="Output file"
1410arg "<extra>" help="Extra arg without env"
1411arg "[default]" help="Arg with default value" default="default value"
1412 "# }
1413 .unwrap();
1414
1415 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1416 Usage: testcli <ARGS>…
1417
1418 Arguments:
1419 <input> Input file [env: MY_INPUT]
1420 <output> Output file [env: MY_OUTPUT]
1421 <extra> Extra arg without env
1422 [default] Arg with default value (default: default value)
1423
1424 Flags:
1425 -h, --help Print help
1426 ");
1427
1428 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1429 Usage: testcli <ARGS>…
1430
1431 Arguments:
1432 <input> Input file
1433 [env: MY_INPUT]
1434 <output> Output file
1435 [env: MY_OUTPUT]
1436 <extra> Extra arg without env
1437 [default] Arg with default value
1438 (default: default value)
1439
1440 Flags:
1441 -h, --help Print help
1442 ");
1443 }
1444
1445 #[test]
1446 fn test_render_help_with_negated_flag() {
1447 let spec = crate::spec! { r#"
1448bin "testcli"
1449flag "--compress" negate="--no-compress" default=#true help="Compress output"
1450flag "--verbose" help="Verbose output"
1451 "# }
1452 .unwrap();
1453
1454 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1455 Usage: testcli [--compress] [--verbose]
1456
1457 Flags:
1458 --compress / --no-compress Compress output (default: true)
1459 --verbose Verbose output
1460 -h, --help Print help
1461 ");
1462
1463 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1464 Usage: testcli [--compress] [--verbose]
1465
1466 Flags:
1467 --compress / --no-compress Compress output
1468 (default: true)
1469 --verbose Verbose output
1470 -h, --help Print help
1471 ");
1472 }
1473
1474 #[test]
1475 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1476 let spec = crate::spec! { r#"
1477bin "testcli"
1478flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1479 choices {
1480 choice "fast"
1481 choice "slow"
1482 }
1483}
1484flag "--short-only" help="short" hide_long_help=#true
1485flag "--long-only" help="long" hide_short_help=#true
1486arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1487 "# }
1488 .unwrap();
1489
1490 let short = render_help(&spec, &spec.cmd, false);
1491 assert!(short.contains("--mode <mode>"), "{short}");
1492 assert!(short.contains("--short-only"), "{short}");
1493 assert!(!short.contains("--long-only"), "{short}");
1494 assert!(
1495 !short.contains("MODE") && !short.contains("fast, slow"),
1496 "{short}"
1497 );
1498 assert!(
1499 !short.contains("default: fast") && !short.contains("default: file"),
1500 "{short}"
1501 );
1502
1503 let long = render_help(&spec, &spec.cmd, true);
1504 assert!(long.contains("--long-only"), "{long}");
1505 assert!(!long.contains("--short-only"), "{long}");
1506 assert!(
1507 !long.contains("MODE") && !long.contains("possible values"),
1508 "{long}"
1509 );
1510
1511 let rendered = spec.to_string();
1512 let reparsed: crate::Spec = rendered.parse().unwrap();
1513 assert!(reparsed.cmd.flags[0].hide_default_value);
1514 assert!(reparsed.cmd.flags[0].hide_env);
1515 assert!(reparsed.cmd.flags[0].hide_possible_values);
1516 }
1517
1518 #[test]
1519 fn a_help_template_reorders_omits_and_wraps_the_sections() {
1520 let spec = crate::spec! { r#"
1524bin "ex"
1525about "An example"
1526help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1527flag "--force" help="Do it anyway"
1528arg "<file>" help="Which file"
1529cmd "run" help="Run it"
1530 "# }
1531 .unwrap();
1532
1533 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1534 An example
1535
1536 Usage: ex [--force] <file> <SUBCOMMAND>
1537
1538 Flags:
1539 --force Do it anyway
1540 -h, --help Print help
1541
1542 Arguments:
1543 <file> Which file
1544
1545 -- ask a person --
1546 ");
1547 }
1548
1549 #[test]
1550 fn a_template_places_the_sections_a_page_actually_has() {
1551 let spec = crate::spec! { r#"
1557bin "ex"
1558version "1.2.3"
1559about "An example"
1560after_help "Read the docs."
1561help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1562flag "--force" help="Do it anyway"
1563cmd "run" help="Run it"
1564 "# }
1565 .unwrap();
1566
1567 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1568 Usage: ex [--force] <SUBCOMMAND>
1569
1570 Flags:
1571 --force Do it anyway
1572 -h, --help Print help
1573 -V, --version Print version
1574
1575 Commands:
1576 run Run it
1577 help Print this message or the help of the given subcommand(s)
1578
1579 Read the docs.
1580
1581 ex 1.2.3
1582 An example
1583 ");
1584 }
1585
1586 #[test]
1587 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1588 let spec = crate::spec! { r#"
1591bin "ex"
1592flatten_help #true
1593help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1594cmd "run" help="Run it" {
1595 flag "--dry-run" help="Only show changes"
1596}
1597 "# }
1598 .unwrap();
1599
1600 let page = render_help(&spec, &spec.cmd, false);
1601 assert!(
1602 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1603 "{page}"
1604 );
1605 assert!(page.contains("--dry-run"), "{page}");
1606 }
1607
1608 #[test]
1609 fn test_render_help_with_before_after_help() {
1610 let spec = crate::spec! { r#"
1611bin "testcli"
1612before_help "This text appears before the help"
1613after_help "This text appears after the help"
1614flag "--verbose" help="Enable verbose output"
1615 "# }
1616 .unwrap();
1617
1618 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1619 This text appears before the help
1620
1621 Usage: testcli [--verbose]
1622
1623 Flags:
1624 --verbose Enable verbose output
1625 -h, --help Print help
1626
1627 This text appears after the help
1628 ");
1629 }
1630
1631 #[test]
1632 fn test_render_help_with_before_after_help_long() {
1633 let spec = crate::spec! { r#"
1634bin "testcli"
1635before_help "short before"
1636before_help_long "This is the long version of before help"
1637after_help "short after"
1638after_help_long "This is the long version of after help"
1639flag "--verbose" help="Enable verbose output"
1640 "# }
1641 .unwrap();
1642
1643 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1644 short before
1645
1646 Usage: testcli [--verbose]
1647
1648 Flags:
1649 --verbose Enable verbose output
1650 -h, --help Print help
1651
1652 short after
1653 ");
1654
1655 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1656 This is the long version of before help
1657
1658 Usage: testcli [--verbose]
1659
1660 Flags:
1661 --verbose Enable verbose output
1662 -h, --help Print help
1663
1664 This is the long version of after help
1665 ");
1666 }
1667
1668 #[test]
1669 fn test_render_help_with_examples() {
1670 let spec = crate::spec! { r#"
1671bin "testcli"
1672flag "--verbose" help="Enable verbose output"
1673example "testcli --verbose" header="Run with verbose output"
1674example "testcli" header="Run normally" help="Just runs the tool"
1675 "# }
1676 .unwrap();
1677
1678 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1679 Usage: testcli [--verbose]
1680
1681 Flags:
1682 --verbose Enable verbose output
1683 -h, --help Print help
1684
1685 Examples:
1686 Run with verbose output:
1687 $ testcli --verbose
1688 Run normally:
1689 $ testcli
1690 ");
1691
1692 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1693 Usage: testcli [--verbose]
1694
1695 Flags:
1696 --verbose Enable verbose output
1697 -h, --help Print help
1698
1699 Examples:
1700 Run with verbose output:
1701 $ testcli --verbose
1702 Run normally:
1703 Just runs the tool
1704 $ testcli
1705 ");
1706 }
1707
1708 #[test]
1709 fn test_render_help_with_version() {
1710 let spec = crate::spec! { r#"
1711bin "testcli"
1712name "TestCLI"
1713version "1.2.3"
1714flag "--verbose" help="Enable verbose output"
1715 "# }
1716 .unwrap();
1717
1718 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1719 TestCLI 1.2.3
1720 Usage: testcli [--verbose]
1721
1722 Flags:
1723 --verbose Enable verbose output
1724 -h, --help Print help
1725 -V, --version Print version
1726 ");
1727 }
1728
1729 #[test]
1730 fn test_render_help_with_only_long_version() {
1731 let spec = crate::spec! { r#"
1732bin "testcli"
1733long_version "1.2.3\ncommit abc123"
1734flag "--verbose" help="Enable verbose output"
1735 "# }
1736 .unwrap();
1737
1738 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1739 Usage: testcli [--verbose]
1740
1741 Flags:
1742 --verbose Enable verbose output
1743 -h, --help Print help
1744 -V, --version Print version
1745 ");
1746 }
1747
1748 #[test]
1749 fn test_render_help_omits_help_when_disabled() {
1750 let spec = crate::spec! { r#"
1754bin "testcli"
1755version "1.2.3"
1756disable_help #true
1757flag "--verbose" help="Enable verbose output"
1758 "# }
1759 .unwrap();
1760
1761 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1762 testcli 1.2.3
1763 Usage: testcli [--verbose]
1764
1765 Flags:
1766 --verbose Enable verbose output
1767 -V, --version Print version
1768 ");
1769 }
1770
1771 #[test]
1772 fn test_render_help_with_author_license() {
1773 let spec = crate::spec! { r#"
1774bin "testcli"
1775author "Test Author"
1776license "MIT"
1777flag "--verbose" help="Enable verbose output"
1778 "# }
1779 .unwrap();
1780
1781 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1783 Usage: testcli [--verbose]
1784
1785 Flags:
1786 --verbose Enable verbose output
1787 -h, --help Print help
1788 ");
1789
1790 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1792 Usage: testcli [--verbose]
1793
1794 Flags:
1795 --verbose Enable verbose output
1796 -h, --help Print help
1797
1798 Author: Test Author
1799 License: MIT
1800 ");
1801 }
1802
1803 #[test]
1804 fn test_render_help_with_deprecated_command() {
1805 let spec = crate::spec! { r#"
1806bin "testcli"
1807flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
1808cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
1809cmd "new-cmd" help="Do something better"
1810 "# }
1811 .unwrap();
1812
1813 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1814 Usage: testcli [--old] <SUBCOMMAND>
1815
1816 Commands:
1817 new-cmd Do something better
1818 old-cmd Do something [deprecated: use new-cmd instead; warns at 6.2; removed
1819 at 7.0]
1820 help Print this message or the help of the given subcommand(s)
1821
1822 Flags:
1823 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
1824 -h, --help Print help
1825 ");
1826 }
1827
1828 #[test]
1829 fn deprecation_milestones_do_not_need_a_message() {
1830 let spec = crate::spec! { r#"
1831bin "testcli"
1832flag "--old" help="Old switch" deprecated_remove_at="7.0"
1833cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
1834 "# }
1835 .unwrap();
1836
1837 let page = render_help(&spec, &spec.cmd, false);
1838 assert!(
1839 page.contains("old-cmd Do something [deprecated: warns at 6.2]"),
1840 "{page}"
1841 );
1842 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
1843 assert!(!page.contains("[deprecated:;"), "{page}");
1844 }
1845
1846 #[test]
1847 fn test_render_help_with_subcommand_presentation() {
1848 let spec = crate::spec! { r#"
1849bin "testcli"
1850subcommand_help_heading "Actions"
1851subcommand_value_name "ACTION"
1852cmd "run" help="Run it\n"
1853 "# }
1854 .unwrap();
1855
1856 let page = render_help(&spec, &spec.cmd, false);
1857 assert!(page.contains("Usage: testcli <ACTION>"), "{page}");
1858 assert!(page.contains("\nActions:\n"), "{page}");
1859 }
1860
1861 #[test]
1862 fn test_render_help_honors_explicit_display_order() {
1863 let spec = crate::spec! { r#"
1864bin "testcli"
1865flag "--unset" help="Unordered"
1866flag "--later" help="Later" display_order=20
1867flag "--first" help="First" display_order=10
1868cmd "zulu" help="Unordered"
1869cmd "later" help="Later" display_order=20
1870cmd "first" help="First" display_order=10
1871cmd "alpha" help="Unordered"
1872 "# }
1873 .unwrap();
1874
1875 let page = render_help(&spec, &spec.cmd, false);
1876 let commands = page.split_once("\nCommands:\n").unwrap().1;
1877 assert!(
1878 commands.find("first").unwrap() < commands.find("later").unwrap()
1879 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
1880 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
1881 "{page}"
1882 );
1883 let flags = page.split_once("\nFlags:\n").unwrap().1;
1884 assert!(
1885 flags.find("--first").unwrap() < flags.find("--later").unwrap()
1886 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
1887 "{page}"
1888 );
1889 }
1890
1891 #[test]
1892 fn test_render_help_groups_subcommands_by_heading() {
1893 let spec = crate::spec! { r#"
1894bin "testcli"
1895cmd "run" help="Run it" help_heading="Core commands"
1896cmd "clean" help="Remove old state" help_heading="Maintenance"
1897cmd "status" help="Show status" help_heading="Commands"
1898 "# }
1899 .unwrap();
1900
1901 for page in [
1902 render_help(&spec, &spec.cmd, false),
1903 render_help(&spec, &spec.cmd, true),
1904 ] {
1905 let commands = page.find("\nCommands:\n").expect("default command section");
1906 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
1907 let core = page.find("\nCore commands:\n").expect("core section");
1908 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
1909 assert!(commands < core && commands < maintenance, "{page}");
1910 let default_end = core.min(maintenance);
1911 assert!(page[commands..default_end].contains("status"), "{page}");
1912 assert!(page[commands..default_end].contains("help"), "{page}");
1913 assert!(page[core..].contains("run"), "{page}");
1914 assert!(page[maintenance..].contains("clean"), "{page}");
1915 }
1916 }
1917
1918 #[test]
1919 fn test_render_help_with_next_line_layout() {
1920 let spec = crate::spec! { r#"
1921bin "testcli"
1922next_line_help #true
1923arg "<input>" help="Input file" env="INPUT" default="fast" {
1924 choices {
1925 choice "fast"
1926 choice "slow"
1927 }
1928}
1929flag "--verbose" help="Enable verbose output"
1930cmd "run" help="Run it"
1931 "# }
1932 .unwrap();
1933
1934 let short = render_help(&spec, &spec.cmd, false);
1935 assert!(!short.contains(" Run it\n\n help"), "{short}");
1936 for page in [short, render_help(&spec, &spec.cmd, true)] {
1937 assert!(page.contains(" [input]\n Input file"), "{page}");
1938 assert!(
1939 page.contains("--verbose\n Enable verbose output"),
1940 "{page}"
1941 );
1942 assert!(
1943 page.contains(
1944 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
1945 ),
1946 "{page}"
1947 );
1948 assert!(page.contains(" run\n Run it"), "{page}");
1949 }
1950 }
1951
1952 #[test]
1953 fn flatten_help_expands_subcommands_instead_of_listing_them() {
1954 let spec = crate::spec! { r#"
1955bin "testcli"
1956flatten_help #true
1957next_line_help #true
1958cmd "run" help="Run it" {
1959 arg "<task>" help="Task name" env="TASK" default="build" {
1960 choices {
1961 choice "build"
1962 choice "test"
1963 }
1964 }
1965 flag "--dry-run" help="Only show changes"
1966 flatten_help #true
1967 cmd "nested" help="Nested operation" {
1968 flag "--deep" help="Deep option"
1969 }
1970}
1971 "# }
1972 .unwrap();
1973
1974 for page in [
1975 render_help(&spec, &spec.cmd, false),
1976 render_help(&spec, &spec.cmd, true),
1977 ] {
1978 assert!(
1979 page.contains("Usage: testcli\n testcli run"),
1980 "{page}"
1981 );
1982 assert!(!page.contains("\nCommands:\n"), "{page}");
1983 assert!(page.contains("\nrun:\nRun it"), "{page}");
1984 assert!(page.contains("[task]"), "{page}");
1985 assert!(page.contains("--dry-run"), "{page}");
1986 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
1987 assert!(page.contains("--deep"), "{page}");
1988 assert!(
1989 page.contains(
1990 " [possible values: build, test]\n [env: TASK]\n (default: build)"
1991 ),
1992 "{page}"
1993 );
1994 }
1995 }
1996
1997 #[test]
1998 fn styled_help_colours_semantics_and_template_styles() {
1999 let spec = crate::spec! { r#"
2000bin "testcli"
2001help_template "{$bright-blue}My tool{/$}\n\n{{usage}}\n\n{{flags}}"
2002flag "--output <FILE>" help="Write **the file**"
2003 "# }
2004 .unwrap();
2005
2006 let plain = render_help(&spec, &spec.cmd, true);
2007 assert!(plain.contains("My tool"), "{plain}");
2008 assert!(!plain.contains('\u{1b}'), "{plain:?}");
2009
2010 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2011 assert!(
2012 coloured.contains("\u{1b}[94mMy tool\u{1b}[0m"),
2013 "{coloured:?}"
2014 );
2015 assert!(
2016 coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"),
2017 "{coloured:?}"
2018 );
2019 assert!(
2020 coloured.contains("\u{1b}[1;32m--output\u{1b}[0m"),
2021 "{coloured:?}"
2022 );
2023 assert!(
2024 coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"),
2025 "{coloured:?}"
2026 );
2027 assert!(
2028 coloured.contains("\u{1b}[1mthe file\u{1b}[22m"),
2029 "{coloured:?}"
2030 );
2031 }
2032}