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