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