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