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 items: supplied,
55 },
56 ),
57 }
58 }
59 }
60
61 let width = crate::docs::layout::help_width(cmd.term_width, cmd.max_term_width);
62 let col = crate::docs::layout::usage_column_width(
63 docs_cmd
64 .flag_groups
65 .iter()
66 .flat_map(|g| g.items.iter())
67 .chain(inherited.iter())
68 .map(|f| f.display_usage.as_str()),
69 width,
70 );
71 let flag_column = Column {
72 width,
73 col,
74 long,
75 next_line: cmd.next_line_help,
76 };
77 for group in &mut docs_cmd.flag_groups {
78 lay_out(&mut group.items, flag_column);
79 }
80 lay_out(&mut inherited, flag_column);
81
82 let arg_col = crate::docs::layout::usage_column_width(
86 docs_cmd.args.iter().map(|a| a.usage.as_str()),
87 width,
88 );
89 for group in &mut docs_cmd.arg_groups {
90 lay_out_args(
91 &mut group.items,
92 Column {
93 width,
94 col: arg_col,
95 long,
96 next_line: cmd.next_line_help,
97 },
98 );
99 }
100
101 lay_out_flattened(&mut docs_cmd.flattened_subcommands, width, long);
104
105 let help_row = {
110 let show_help_row = !docs_cmd.subcommands.is_empty()
111 && !docs_cmd.flatten_help
112 && !cmd.disable_help_subcommand;
113 let cmd_col = crate::docs::layout::usage_column_width(
114 docs_cmd
115 .subcommand_groups
116 .iter()
117 .flat_map(|g| g.items.iter())
118 .map(|c| c.name.as_str())
119 .chain(show_help_row.then_some(HELP_SUBCOMMAND)),
120 width,
121 );
122 for group in &mut docs_cmd.subcommand_groups {
123 lay_out_commands(&mut group.items, width, cmd_col);
124 }
125 show_help_row.then(|| {
129 render_row(
130 HELP_SUBCOMMAND,
131 HELP_SUBCOMMAND_SUMMARY,
132 width,
133 cmd_col,
134 cmd.next_line_help,
135 )
136 })
137 };
138 ctx.insert("help_row", &help_row);
139
140 let arg_has_ungrouped = docs_cmd
141 .arg_groups
142 .iter()
143 .any(|group| group.heading.is_none());
144 let arg_has_grouped = docs_cmd
145 .arg_groups
146 .iter()
147 .any(|group| group.heading.is_some());
148 let flag_has_ungrouped = docs_cmd
149 .flag_groups
150 .iter()
151 .any(|group| group.heading.is_none());
152 let flag_has_grouped = docs_cmd
153 .flag_groups
154 .iter()
155 .any(|group| group.heading.is_some());
156 ctx.insert("arg_has_ungrouped", &arg_has_ungrouped);
157 ctx.insert("arg_has_grouped", &arg_has_grouped);
158 ctx.insert("flag_has_ungrouped", &flag_has_ungrouped);
159 ctx.insert("flag_has_grouped", &flag_has_grouped);
160
161 ctx.insert("cmd", &docs_cmd);
164 ctx.insert("global_flags", &inherited);
165 for (name, mark) in MARKS {
166 ctx.insert(name, &mark);
167 }
168 ctx.insert("mark_grouped_args", &MARK_GROUPED_ARGS);
169 ctx.insert("mark_grouped_flags", &MARK_GROUPED_FLAGS);
170 ctx.insert("mark_global_flags", &MARK_GLOBAL_FLAGS);
171 let template = if long {
172 "spec_template_long.tera"
173 } else {
174 "spec_template_short.tera"
175 };
176 let rendered = TERA.render(template, &ctx).unwrap();
177 let sections = Sections::split(&rendered);
178 let page = match spec
179 .help_template
180 .as_deref()
181 .filter(|t| crate::help_template::is_set(t))
182 {
183 Some(template) => crate::help_template::substitute(template, |name| sections.named(name)),
184 None => sections.concatenated(),
185 };
186 page.trim().to_string() + "\n"
187}
188
189const MARKS: [(&str, &str); 6] = [
200 ("mark_usage", "\u{1}usage\u{1}"),
201 ("mark_commands", "\u{1}commands\u{1}"),
202 ("mark_args", "\u{1}args\u{1}"),
203 ("mark_flags", "\u{1}flags\u{1}"),
204 ("mark_flattened", "\u{1}flattened\u{1}"),
205 ("mark_after_help", "\u{1}after_help\u{1}"),
206];
207const MARK_GROUPED_ARGS: &str = "\u{1}grouped_args\u{1}";
208const MARK_GROUPED_FLAGS: &str = "\u{1}grouped_flags\u{1}";
209const MARK_GLOBAL_FLAGS: &str = "\u{1}global_flags\u{1}";
210
211struct Sections<'a> {
217 about: &'a str,
218 usage: &'a str,
219 commands: &'a str,
220 args: String,
221 flags: String,
222 grouped_args: &'a str,
223 ungrouped_args: &'a str,
224 grouped_flags: &'a str,
225 ungrouped_flags: String,
226 flattened: &'a str,
227 after_help: &'a str,
228}
229
230impl<'a> Sections<'a> {
231 fn split(rendered: &'a str) -> Self {
232 let mut rest = rendered;
233 let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
234 for (_, mark) in MARKS {
235 match rest.split_once(mark) {
239 Some((before, after)) => {
240 parts.push(before);
241 rest = after;
242 }
243 None => parts.push(""),
244 }
245 }
246 parts.push(rest);
247 let (ungrouped_args, grouped_args) = parts[3]
248 .split_once(MARK_GROUPED_ARGS)
249 .unwrap_or((parts[3], ""));
250 let (own_flags, global_flags) = parts[4]
251 .split_once(MARK_GLOBAL_FLAGS)
252 .unwrap_or((parts[4], ""));
253 let (own_ungrouped_flags, grouped_flags) = own_flags
254 .split_once(MARK_GROUPED_FLAGS)
255 .unwrap_or((own_flags, ""));
256 Self {
257 about: parts[0],
258 usage: parts[1],
259 commands: parts[2],
260 args: format!("{ungrouped_args}{grouped_args}"),
261 flags: format!("{own_ungrouped_flags}{grouped_flags}{global_flags}"),
262 grouped_args,
263 ungrouped_args,
264 grouped_flags,
265 ungrouped_flags: format!("{own_ungrouped_flags}{global_flags}"),
266 flattened: parts[5],
267 after_help: parts[6],
268 }
269 }
270
271 fn concatenated(&self) -> String {
273 [
274 self.about,
275 self.usage,
276 self.commands,
277 self.args.as_str(),
278 self.flags.as_str(),
279 self.flattened,
280 self.after_help,
281 ]
282 .concat()
283 }
284
285 fn named(&self, name: &str) -> Option<String> {
287 Some(match name {
288 "about" => self.about.trim().to_string(),
289 "usage" => self.usage.trim().to_string(),
290 "commands" => {
291 let mut out = self.commands.trim().to_string();
292 let flattened = self.flattened.trim();
293 if !flattened.is_empty() {
294 if !out.is_empty() {
295 out.push_str("\n\n");
296 }
297 out.push_str(flattened);
298 }
299 out
300 }
301 "args" => self.args.trim().to_string(),
302 "flags" => self.flags.trim().to_string(),
303 "grouped_args" => self.grouped_args.trim().to_string(),
304 "ungrouped_args" => self.ungrouped_args.trim().to_string(),
305 "grouped_flags" => self.grouped_flags.trim().to_string(),
306 "ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
307 "after_help" => self.after_help.trim().to_string(),
308 _ => return None,
309 })
310 }
311}
312
313fn supplied_flags(
327 spec: &Spec,
328 cmd: &SpecCommand,
329 ancestors_taken: &[String],
330 is_root: bool,
331) -> Vec<crate::docs::models::SpecFlag> {
332 let mut taken: Vec<String> = ancestors_taken.to_vec();
337 for f in &cmd.flags {
338 taken.extend(f.long.iter().map(|l| format!("--{l}")));
339 taken.extend(f.short.iter().map(|s| format!("-{s}")));
340 taken.extend(f.negate.clone());
342 }
343
344 let build = |name: &str, long: &str, short: char, help: &str| {
345 let long_free = !taken.contains(&format!("--{long}"));
346 let short_free = !taken.contains(&format!("-{short}"));
347 if !long_free && !short_free {
348 return None;
349 }
350 let name = if long_free { name } else { &short.to_string() };
353 let mut flag = crate::SpecFlag {
354 name: name.to_string(),
355 long: if long_free {
356 vec![long.to_string()]
357 } else {
358 vec![]
359 },
360 short: if short_free { vec![short] } else { vec![] },
361 help: Some(help.to_string()),
362 ..Default::default()
363 };
364 flag.usage = flag.usage();
365 Some(crate::docs::models::SpecFlag::from(&flag))
366 };
367
368 let mut out = Vec::new();
369 if spec.disable_help != Some(true) && !cmd.disable_help_flag {
376 out.extend(build("help", "help", 'h', "Print help"));
377 }
378 if is_root
379 && (spec.version.is_some() || spec.long_version.is_some())
380 && !cmd.disable_version_flag
381 {
382 out.extend(build("version", "version", 'V', "Print version"));
383 }
384 out
385}
386
387fn lay_out(flags: &mut [crate::docs::models::SpecFlag], column: Column) {
394 for flag in flags {
395 flag.usage_col_width = column.col;
396 flag.help_is_block =
397 column.is_block(crate::docs::layout::visible_width(&flag.display_usage));
398 let text = if column.long {
399 flag.help_long
400 .as_deref()
401 .or(flag.help.as_deref())
402 .map(str::to_string)
403 } else {
404 with_annotations(flag.help.as_deref(), flag_annotations(flag))
405 };
406 wrap_into(
407 text,
408 column,
409 crate::docs::layout::visible_width(&flag.display_usage),
410 &mut flag.row,
411 &mut flag.help_rendered,
412 &mut flag.help_is_multiline,
413 &mut flag.ann_indent,
414 );
415 }
416}
417
418fn lay_out_args(args: &mut [crate::docs::models::SpecArg], column: Column) {
424 for arg in args {
425 arg.usage_col_width = column.col;
426 arg.help_is_block = column.is_block(crate::docs::layout::visible_width(&arg.usage));
427 let text = if column.long {
428 arg.help_long
429 .as_deref()
430 .or(arg.help.as_deref())
431 .map(str::to_string)
432 } else {
433 with_annotations(arg.help.as_deref(), arg_annotations(arg))
434 };
435 wrap_into(
436 text,
437 column,
438 crate::docs::layout::visible_width(&arg.usage),
439 &mut arg.row,
440 &mut arg.help_rendered,
441 &mut arg.help_is_multiline,
442 &mut arg.ann_indent,
443 );
444 }
445}
446
447fn lay_out_flattened(commands: &mut [crate::docs::models::SpecCommand], width: usize, long: bool) {
448 for command in commands {
449 let arg_col = crate::docs::layout::usage_column_width(
450 command
451 .arg_groups
452 .iter()
453 .flat_map(|group| group.items.iter())
454 .map(|arg| arg.usage.as_str()),
455 width,
456 );
457 let arg_column = Column {
458 width,
459 col: arg_col,
460 long,
461 next_line: command.flattened_next_line_help,
462 };
463 for group in &mut command.arg_groups {
464 lay_out_args(&mut group.items, arg_column);
465 }
466
467 let flag_col = crate::docs::layout::usage_column_width(
468 command
469 .flag_groups
470 .iter()
471 .flat_map(|group| group.items.iter())
472 .map(|flag| flag.display_usage.as_str()),
473 width,
474 );
475 let flag_column = Column {
476 width,
477 col: flag_col,
478 long,
479 next_line: command.flattened_next_line_help,
480 };
481 for group in &mut command.flag_groups {
482 lay_out(&mut group.items, flag_column);
483 }
484 }
485}
486
487fn wrap_into(
493 text: Option<String>,
494 column: Column,
495 usage_width: usize,
496 row: &mut Option<String>,
497 help_rendered: &mut Option<String>,
498 help_is_multiline: &mut bool,
499 ann_indent: &mut String,
500) {
501 *row = None;
502 *help_rendered = None;
503 *help_is_multiline = false;
504 *ann_indent = column.annotation_indent(!column.is_block(usage_width));
507 let Some(text) = text else { return };
508 if column.is_block(usage_width) {
509 *row = Some(
510 if column.usage_overflows(usage_width) && !text.contains('\n') {
511 crate::docs::layout::render_block_text(&text, column.width)
512 } else {
513 text
514 },
515 );
516 return;
517 }
518 let (rendered, is_multiline) =
519 crate::docs::layout::render_help_text(&text, column.width, column.col);
520 *ann_indent = column.annotation_indent(!rendered.is_empty());
524 if !rendered.is_empty() {
525 *help_rendered = Some(rendered);
526 *help_is_multiline = is_multiline;
527 }
528 *row = Some(text);
529}
530
531fn with_annotations(help: Option<&str>, annotations: Vec<String>) -> Option<String> {
538 let mut parts = Vec::new();
539 if let Some(help) = summarize(help) {
540 parts.push(help.to_string());
541 }
542 parts.extend(annotations);
543 (!parts.is_empty()).then(|| parts.join(" "))
544}
545
546fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec<String> {
548 let mut parts = value_annotations(
549 flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()),
550 flag.hide_possible_values,
551 flag.env.as_deref(),
552 flag.hide_env,
553 &flag.env_fallback,
554 &flag.deprecated_env,
555 &flag.default,
556 flag.hide_default_value,
557 );
558 if let Some(label) = deprecation_label(
559 flag.deprecated.as_deref(),
560 flag.deprecated_warn_at.as_deref(),
561 flag.deprecated_remove_at.as_deref(),
562 ) {
563 parts.push(label);
564 }
565 parts
566}
567
568fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec<String> {
570 value_annotations(
571 arg.choices.as_ref(),
572 arg.hide_possible_values,
573 arg.env.as_deref(),
574 arg.hide_env,
575 &arg.env_fallback,
576 &arg.deprecated_env,
577 &arg.default,
578 arg.hide_default_value,
579 )
580}
581
582#[allow(clippy::too_many_arguments)]
584fn value_annotations(
585 choices: Option<&crate::SpecChoices>,
586 hide_possible_values: bool,
587 env: Option<&str>,
588 hide_env: bool,
589 env_fallback: &[String],
590 deprecated_env: &[String],
591 default: &[String],
592 hide_default_value: bool,
593) -> Vec<String> {
594 let mut parts = Vec::new();
595 if let Some(choices) = choices.filter(|_| !hide_possible_values) {
596 if !choices.choices.is_empty() {
597 parts.push(format!("[{}]", choices.choices.join(", ")));
598 }
599 if let Some(env) = choices.env() {
600 parts.push(format!("[choices env: {env}]"));
601 }
602 }
603 if !hide_env {
604 if let Some(env) = env {
605 parts.push(format!("[env: {env}]"));
606 }
607 parts.extend(
608 env_fallback
609 .iter()
610 .map(|env| format!("[env fallback: {env}]")),
611 );
612 parts.extend(
613 deprecated_env
614 .iter()
615 .map(|env| format!("[deprecated env: {env}]")),
616 );
617 }
618 if !hide_default_value && !default.is_empty() {
619 parts.push(format!("(default: {})", default.join(", ")));
620 }
621 parts
622}
623
624#[derive(Clone, Copy)]
626struct Column {
627 width: usize,
629 col: usize,
631 long: bool,
633 next_line: bool,
635}
636
637const BLOCK_INDENT: usize = 4;
639
640impl Column {
641 fn usage_overflows(&self, usage_width: usize) -> bool {
642 !self.next_line && usage_width > self.col
643 }
644
645 fn is_block(&self, usage_width: usize) -> bool {
648 self.next_line
649 || self.usage_overflows(usage_width)
650 || self.width.saturating_sub(2 + self.col + 2) < 10
651 }
652
653 fn annotation_indent(&self, reached_column: bool) -> String {
660 let indent = if reached_column {
661 2 + self.col + 2
662 } else {
663 BLOCK_INDENT
664 };
665 " ".repeat(indent)
666 }
667}
668
669const HELP_SUBCOMMAND: &str = "help";
671const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
672
673fn lay_out_commands(
678 commands: &mut [crate::docs::models::HelpCommand],
679 terminal_width: usize,
680 col: usize,
681) {
682 for command in commands {
683 command.usage_col_width = col;
684 command.row = command_row(command);
685 command.help_rendered = None;
686 command.help_is_multiline = false;
687 if crate::docs::layout::visible_width(&command.name) > col {
688 if let Some(row) = command.row.as_mut().filter(|row| !row.contains('\n')) {
689 *row = crate::docs::layout::render_block_text(row, terminal_width);
690 }
691 continue;
692 }
693 if let Some(row) = command.row.as_deref() {
694 let (rendered, is_multiline) =
695 crate::docs::layout::render_help_text(row, terminal_width, col);
696 if !rendered.is_empty() {
697 command.help_rendered = Some(rendered);
698 command.help_is_multiline = is_multiline;
699 }
700 }
701 }
702}
703
704fn command_row(cmd: &crate::docs::models::HelpCommand) -> Option<String> {
711 let mut parts = Vec::new();
712 let summary = summarize(cmd.help.as_deref()).or_else(|| {
715 summarize(
716 cmd.help_long
717 .as_deref()
718 .and_then(|help| help.lines().next()),
719 )
720 });
721 if let Some(summary) = summary {
722 parts.push(summary.to_string());
723 }
724 if !cmd.aliases.is_empty() {
725 parts.push(format!("[aliases: {}]", cmd.aliases.join(", ")));
726 }
727 if let Some(label) = deprecation_label(
728 cmd.deprecated.as_deref(),
729 cmd.deprecated_warn_at.as_deref(),
730 cmd.deprecated_remove_at.as_deref(),
731 ) {
732 parts.push(label);
733 }
734 (!parts.is_empty()).then(|| parts.join(" "))
735}
736
737fn summarize(text: Option<&str>) -> Option<&str> {
739 text.map(str::trim_end).filter(|text| !text.is_empty())
740}
741
742fn deprecation_label(
744 message: Option<&str>,
745 warn_at: Option<&str>,
746 remove_at: Option<&str>,
747) -> Option<String> {
748 if message.is_none() && warn_at.is_none() && remove_at.is_none() {
749 return None;
750 }
751 let mut parts = Vec::new();
752 if let Some(message) = message {
753 parts.push(message.to_string());
754 }
755 if let Some(at) = warn_at {
756 parts.push(format!("warns at {at}"));
757 }
758 if let Some(at) = remove_at {
759 parts.push(format!("removed at {at}"));
760 }
761 Some(format!("[deprecated: {}]", parts.join("; ")))
762}
763
764fn render_row(
766 name: &str,
767 row: &str,
768 terminal_width: usize,
769 col: usize,
770 next_line_help: bool,
771) -> String {
772 if !next_line_help {
773 if crate::docs::layout::visible_width(name) <= col {
774 let (rendered, _) = crate::docs::layout::render_help_text(row, terminal_width, col);
775 if !rendered.is_empty() {
776 return format!(" {name:<col$} {rendered}");
777 }
778 } else if !row.contains('\n') {
779 return format!(
780 " {name}\n {}",
781 crate::docs::layout::render_block_text(row, terminal_width)
782 );
783 }
784 }
785 format!(" {name}\n {row}")
786}
787
788fn inherited_flags(
797 spec: &Spec,
798 cmd: &SpecCommand,
799 full_cmd: &[String],
800 long_help: bool,
801) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
802 let mut ancestors: Vec<&SpecCommand> = Vec::new();
804 let mut at = &spec.cmd;
805 for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
806 ancestors.push(at);
807 let Some(next) = at.subcommands.get(name) else {
808 return (Vec::new(), Vec::new());
809 };
810 at = next;
811 }
812 if !full_cmd.is_empty() {
813 ancestors.push(at);
814 }
815
816 let forms = |f: &crate::SpecFlag| -> Vec<String> {
829 f.long
830 .iter()
831 .map(|l| format!("--{l}"))
832 .chain(f.short.iter().map(|s| format!("-{s}")))
833 .collect()
834 };
835 let every_form: Vec<String> = cmd
836 .flags
837 .iter()
838 .chain(
839 ancestors
840 .iter()
841 .flat_map(|a| a.flags.iter())
842 .filter(|f| f.global),
843 )
844 .flat_map(&forms)
845 .collect();
846
847 let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
848 let mut taken_negations: Vec<String> =
849 cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
850 let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
851 for ancestor in ancestors.iter().rev() {
852 for f in ancestor.flags.iter().filter(|f| f.global) {
853 let long = f
854 .long
855 .iter()
856 .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
857 .cloned();
858 let short = f
859 .short
860 .iter()
861 .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
862 .copied();
863 let mine = forms(f);
864 let negate = f.negate.as_ref().is_some_and(|n| {
865 !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
866 });
867 taken.extend(forms(f));
870 taken_negations.extend(f.negate.clone());
871 if f.hide
872 || if long_help {
873 f.hide_long_help
874 } else {
875 f.hide_short_help
876 }
877 || (long.is_none() && short.is_none() && !negate)
878 {
879 continue;
880 }
881 keep.push((f, long, short, negate));
882 }
883 }
884 let shown: Vec<crate::docs::models::SpecFlag> = ancestors
885 .iter()
886 .flat_map(|a| a.flags.iter())
887 .filter_map(|f| {
888 keep.iter()
889 .find(|(k, _, _, _)| std::ptr::eq(*k, f))
890 .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
891 })
892 .map(|(f, long, short, negate)| {
893 let mut shown = f.clone();
896 shown.long = long.into_iter().collect();
897 shown.short = short.into_iter().collect();
898 if !negate {
899 shown.negate = None;
900 }
901 shown.usage = shown.usage();
902 crate::docs::models::SpecFlag::from(&shown)
903 })
904 .collect();
905 taken.extend(taken_negations);
909 (shown, taken)
910}
911
912fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
924 let mut visible = cmd.clone();
925 visible.flags.retain(|flag| {
926 !flag.hide
927 && if long {
928 !flag.hide_long_help
929 } else {
930 !flag.hide_short_help
931 }
932 });
933 visible.args.retain(|arg| {
934 !arg.hide
935 && if long {
936 !arg.hide_long_help
937 } else {
938 !arg.hide_short_help
939 }
940 });
941 visible.subcommands.retain(|_, sub| !sub.hide);
942 if visible.flatten_help {
948 for sub in visible.subcommands.values_mut() {
949 *sub = without_hidden(sub, long);
950 }
951 }
952 visible
953}
954
955static TERA: LazyLock<Tera> = LazyLock::new(|| {
956 let mut tera = Tera::default();
957
958 tera.register_filter(
960 "ljust",
961 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
962 let value = value.as_str().unwrap_or("");
963 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
964 Ok(format!("{:<width$}", value, width = width))
965 },
966 );
967 tera.register_filter(
968 "default",
969 |value: &tera::Value,
970 kwargs: tera::Kwargs,
971 _: &tera::State|
972 -> tera::TeraResult<tera::Value> {
973 let default_val = kwargs.must_get::<tera::Value>("value")?;
974 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
975 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
976 Ok(default_val)
977 } else {
978 Ok(value.clone())
979 }
980 },
981 );
982
983 #[rustfmt::skip]
984 tera.add_raw_templates([
985 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
986 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
987 ]).unwrap();
988
989 tera
990});
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995 use insta::assert_snapshot;
996
997 #[test]
998 fn flag_aliases_do_not_leak_into_interactive_help() {
999 let spec = crate::spec! { r#"
1000bin "ex"
1001flag "-t -f --tail --follow" help="Follow output"
1002 "# }
1003 .unwrap();
1004
1005 for long in [false, true] {
1006 let page = super::render_help(&spec, &spec.cmd, long);
1007 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1008 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1009 }
1010 }
1011
1012 #[test]
1013 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1014 let spec = crate::spec! { r#"
1019bin "ex"
1020flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1021cmd inner help="a command" {
1022 flag "--plain" help="its own"
1023}
1024 "# }
1025 .unwrap();
1026
1027 let inner = spec.cmd.subcommands.get("inner").expect("inner");
1028 for long in [false, true] {
1029 let page = super::render_help(&spec, inner, long);
1030 assert!(
1031 !page.contains("--help"),
1032 "long={long}: a hidden ancestor binds this:\n{page}"
1033 );
1034 assert!(page.contains("-h"), "long={long}:\n{page}");
1036 }
1037 }
1038
1039 #[test]
1040 fn a_long_beats_a_negation_however_far_away_it_is() {
1041 let spec = crate::spec! { r#"
1047bin "ex"
1048flag "--no-cache" global=#true help="the root's plain long"
1049flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1050cmd narrow help="a command" {
1051 flag "--cache" negate="--no-cache" help="its own, with a negation"
1052 flag "--tint" negate="--no-colour" help="claims the root's negation"
1053}
1054 "# }
1055 .unwrap();
1056
1057 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1058 for long in [false, true] {
1059 let page = super::render_help(&spec, narrow, long);
1060 assert!(
1061 page.contains("--no-cache"),
1062 "long={long}: a long beats a negation, so this still binds here:\n{page}"
1063 );
1064 assert!(page.contains("--colour"), "long={long}:\n{page}");
1067 let global = page
1068 .split_once("Global flags:")
1069 .expect("a global section")
1070 .1;
1071 assert!(
1072 !global.contains("--colour / --no-colour"),
1073 "long={long}: the nearer negation owns that spelling:\n{page}"
1074 );
1075 }
1076 }
1077
1078 #[test]
1079 fn a_description_of_only_spaces_is_no_description() {
1080 let spec = crate::spec! { r#"
1087bin "ex"
1088flag "--blank" help=" "
1089flag "--plain" help="plain"
1090 "# }
1091 .unwrap();
1092
1093 for long in [false, true] {
1094 let page = super::render_help(&spec, &spec.cmd, long);
1095 let listing = page.split_once("\nFlags:").expect("a flags section").1;
1098 let line = listing
1099 .lines()
1100 .find(|l| l.contains("--blank"))
1101 .unwrap_or_else(|| panic!("long={long}: {page}"));
1102 assert_eq!(
1103 line,
1104 line.trim_end(),
1105 "long={long}: trailing space on {line:?}"
1106 );
1107 }
1108 }
1109
1110 #[test]
1111 fn test_render_help_omits_hidden_entries() {
1112 let spec = crate::spec! { r#"
1113bin "ex"
1114flag "--visible" help="shown"
1115flag "--secret" hide=#true help="hidden"
1116flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1117arg "[SHOWN]" help="an arg"
1118arg "[HIDDEN]" hide=#true help="a hidden arg"
1119cmd open help="a command"
1120cmd sneaky hide=#true help="a hidden command"
1121 "# }
1122 .unwrap();
1123
1124 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1129 Usage: ex [--visible] [SHOWN] <SUBCOMMAND>
1130
1131 Commands:
1132 open a command
1133 help Print this message or the help of the given subcommand(s)
1134
1135 Arguments:
1136 [SHOWN] an arg
1137
1138 Flags:
1139 --visible shown
1140 -h, --help Print help
1141 ");
1142 }
1143
1144 #[test]
1145 fn test_render_help_groups_by_heading() {
1146 let spec = crate::spec! { r#"
1147bin "testcli"
1148flag "--verbose" help="Verbose output"
1149flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1150flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1151flag "--jobs <n>" help="How many at once" help_heading="Performance"
1152arg "<file>" help="The file"
1153arg "<mode>" help="How to run" help_heading="Behaviour"
1154 "# }
1155 .unwrap();
1156
1157 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1160 Usage: testcli [FLAGS] <file> <mode>
1161
1162 Arguments:
1163 <file> The file
1164
1165 Behaviour:
1166 <mode> How to run
1167
1168 Flags:
1169 --verbose Verbose output
1170 -h, --help Print help
1171
1172 Filtering:
1173 --filter <pattern> Only matching
1174 --exclude <pattern> Skip matching
1175
1176 Performance:
1177 --jobs <n> How many at once
1178 ");
1179 }
1180
1181 #[test]
1182 fn test_render_help_with_only_headed_flags() {
1183 let spec = crate::spec! { r#"
1186bin "testcli"
1187flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1188 "# }
1189 .unwrap();
1190
1191 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1192 Usage: testcli [--filter <pattern>]
1193
1194 Flags:
1195 -h, --help Print help
1196
1197 Filtering:
1198 --filter <pattern> Only matching
1199 ");
1200 }
1201
1202 #[test]
1203 fn test_render_help_with_env() {
1204 let spec = crate::spec! { r#"
1205bin "testcli"
1206flag "--color" env="MYCLI_COLOR" help="Enable color output"
1207flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1208flag "--debug" help="Debug mode"
1209 "# }
1210 .unwrap();
1211
1212 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1213 Usage: testcli [FLAGS]
1214
1215 Flags:
1216 --color Enable color output [env: MYCLI_COLOR]
1217 --verbose Verbose output [env: MYCLI_VERBOSE]
1218 --debug Debug mode
1219 -h, --help Print help
1220 ");
1221
1222 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1223 Usage: testcli [FLAGS]
1224
1225 Flags:
1226 --color Enable color output
1227 [env: MYCLI_COLOR]
1228 --verbose Verbose output
1229 [env: MYCLI_VERBOSE]
1230 --debug Debug mode
1231 -h, --help Print help
1232 ");
1233 }
1234
1235 #[test]
1236 fn test_render_help_with_arg_env() {
1237 let spec = crate::spec! { r#"
1238bin "testcli"
1239arg "<input>" env="MY_INPUT" help="Input file"
1240arg "<output>" env="MY_OUTPUT" help="Output file"
1241arg "<extra>" help="Extra arg without env"
1242arg "[default]" help="Arg with default value" default="default value"
1243 "# }
1244 .unwrap();
1245
1246 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1247 Usage: testcli <ARGS>…
1248
1249 Arguments:
1250 <input> Input file [env: MY_INPUT]
1251 <output> Output file [env: MY_OUTPUT]
1252 <extra> Extra arg without env
1253 [default] Arg with default value (default: default value)
1254
1255 Flags:
1256 -h, --help Print help
1257 ");
1258
1259 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1260 Usage: testcli <ARGS>…
1261
1262 Arguments:
1263 <input> Input file
1264 [env: MY_INPUT]
1265 <output> Output file
1266 [env: MY_OUTPUT]
1267 <extra> Extra arg without env
1268 [default] Arg with default value
1269 (default: default value)
1270
1271 Flags:
1272 -h, --help Print help
1273 ");
1274 }
1275
1276 #[test]
1277 fn test_render_help_with_negated_flag() {
1278 let spec = crate::spec! { r#"
1279bin "testcli"
1280flag "--compress" negate="--no-compress" default=#true help="Compress output"
1281flag "--verbose" help="Verbose output"
1282 "# }
1283 .unwrap();
1284
1285 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1286 Usage: testcli [--compress] [--verbose]
1287
1288 Flags:
1289 --compress / --no-compress Compress output (default: true)
1290 --verbose Verbose output
1291 -h, --help Print help
1292 ");
1293
1294 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1295 Usage: testcli [--compress] [--verbose]
1296
1297 Flags:
1298 --compress / --no-compress Compress output
1299 (default: true)
1300 --verbose Verbose output
1301 -h, --help Print help
1302 ");
1303 }
1304
1305 #[test]
1306 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1307 let spec = crate::spec! { r#"
1308bin "testcli"
1309flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1310 choices {
1311 choice "fast"
1312 choice "slow"
1313 }
1314}
1315flag "--short-only" help="short" hide_long_help=#true
1316flag "--long-only" help="long" hide_short_help=#true
1317arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1318 "# }
1319 .unwrap();
1320
1321 let short = render_help(&spec, &spec.cmd, false);
1322 assert!(short.contains("--mode <mode>"), "{short}");
1323 assert!(short.contains("--short-only"), "{short}");
1324 assert!(!short.contains("--long-only"), "{short}");
1325 assert!(
1326 !short.contains("MODE") && !short.contains("fast, slow"),
1327 "{short}"
1328 );
1329 assert!(
1330 !short.contains("default: fast") && !short.contains("default: file"),
1331 "{short}"
1332 );
1333
1334 let long = render_help(&spec, &spec.cmd, true);
1335 assert!(long.contains("--long-only"), "{long}");
1336 assert!(!long.contains("--short-only"), "{long}");
1337 assert!(
1338 !long.contains("MODE") && !long.contains("possible values"),
1339 "{long}"
1340 );
1341
1342 let rendered = spec.to_string();
1343 let reparsed: crate::Spec = rendered.parse().unwrap();
1344 assert!(reparsed.cmd.flags[0].hide_default_value);
1345 assert!(reparsed.cmd.flags[0].hide_env);
1346 assert!(reparsed.cmd.flags[0].hide_possible_values);
1347 }
1348
1349 #[test]
1350 fn a_help_template_reorders_omits_and_wraps_the_sections() {
1351 let spec = crate::spec! { r#"
1355bin "ex"
1356about "An example"
1357help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1358flag "--force" help="Do it anyway"
1359arg "<file>" help="Which file"
1360cmd "run" help="Run it"
1361 "# }
1362 .unwrap();
1363
1364 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1365 An example
1366
1367 Usage: ex [--force] <file> <SUBCOMMAND>
1368
1369 Flags:
1370 --force Do it anyway
1371 -h, --help Print help
1372
1373 Arguments:
1374 <file> Which file
1375
1376 -- ask a person --
1377 ");
1378 }
1379
1380 #[test]
1381 fn a_template_places_the_sections_a_page_actually_has() {
1382 let spec = crate::spec! { r#"
1388bin "ex"
1389version "1.2.3"
1390about "An example"
1391after_help "Read the docs."
1392help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1393flag "--force" help="Do it anyway"
1394cmd "run" help="Run it"
1395 "# }
1396 .unwrap();
1397
1398 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1399 Usage: ex [--force] <SUBCOMMAND>
1400
1401 Flags:
1402 --force Do it anyway
1403 -h, --help Print help
1404 -V, --version Print version
1405
1406 Commands:
1407 run Run it
1408 help Print this message or the help of the given subcommand(s)
1409
1410 Read the docs.
1411
1412 ex 1.2.3
1413 An example
1414 ");
1415 }
1416
1417 #[test]
1418 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1419 let spec = crate::spec! { r#"
1422bin "ex"
1423flatten_help #true
1424help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1425cmd "run" help="Run it" {
1426 flag "--dry-run" help="Only show changes"
1427}
1428 "# }
1429 .unwrap();
1430
1431 let page = render_help(&spec, &spec.cmd, false);
1432 assert!(
1433 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1434 "{page}"
1435 );
1436 assert!(page.contains("--dry-run"), "{page}");
1437 }
1438
1439 #[test]
1440 fn test_render_help_with_before_after_help() {
1441 let spec = crate::spec! { r#"
1442bin "testcli"
1443before_help "This text appears before the help"
1444after_help "This text appears after the help"
1445flag "--verbose" help="Enable verbose output"
1446 "# }
1447 .unwrap();
1448
1449 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1450 This text appears before the help
1451
1452 Usage: testcli [--verbose]
1453
1454 Flags:
1455 --verbose Enable verbose output
1456 -h, --help Print help
1457
1458 This text appears after the help
1459 ");
1460 }
1461
1462 #[test]
1463 fn test_render_help_with_before_after_help_long() {
1464 let spec = crate::spec! { r#"
1465bin "testcli"
1466before_help "short before"
1467before_help_long "This is the long version of before help"
1468after_help "short after"
1469after_help_long "This is the long version of after help"
1470flag "--verbose" help="Enable verbose output"
1471 "# }
1472 .unwrap();
1473
1474 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1475 short before
1476
1477 Usage: testcli [--verbose]
1478
1479 Flags:
1480 --verbose Enable verbose output
1481 -h, --help Print help
1482
1483 short after
1484 ");
1485
1486 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1487 This is the long version of before help
1488
1489 Usage: testcli [--verbose]
1490
1491 Flags:
1492 --verbose Enable verbose output
1493 -h, --help Print help
1494
1495 This is the long version of after help
1496 ");
1497 }
1498
1499 #[test]
1500 fn test_render_help_with_examples() {
1501 let spec = crate::spec! { r#"
1502bin "testcli"
1503flag "--verbose" help="Enable verbose output"
1504example "testcli --verbose" header="Run with verbose output"
1505example "testcli" header="Run normally" help="Just runs the tool"
1506 "# }
1507 .unwrap();
1508
1509 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1510 Usage: testcli [--verbose]
1511
1512 Flags:
1513 --verbose Enable verbose output
1514 -h, --help Print help
1515
1516 Examples:
1517 Run with verbose output:
1518 $ testcli --verbose
1519 Run normally:
1520 $ testcli
1521 ");
1522
1523 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1524 Usage: testcli [--verbose]
1525
1526 Flags:
1527 --verbose Enable verbose output
1528 -h, --help Print help
1529
1530 Examples:
1531 Run with verbose output:
1532 $ testcli --verbose
1533 Run normally:
1534 Just runs the tool
1535 $ testcli
1536 ");
1537 }
1538
1539 #[test]
1540 fn test_render_help_with_version() {
1541 let spec = crate::spec! { r#"
1542bin "testcli"
1543name "TestCLI"
1544version "1.2.3"
1545flag "--verbose" help="Enable verbose output"
1546 "# }
1547 .unwrap();
1548
1549 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1550 TestCLI 1.2.3
1551 Usage: testcli [--verbose]
1552
1553 Flags:
1554 --verbose Enable verbose output
1555 -h, --help Print help
1556 -V, --version Print version
1557 ");
1558 }
1559
1560 #[test]
1561 fn test_render_help_with_only_long_version() {
1562 let spec = crate::spec! { r#"
1563bin "testcli"
1564long_version "1.2.3\ncommit abc123"
1565flag "--verbose" help="Enable verbose output"
1566 "# }
1567 .unwrap();
1568
1569 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1570 Usage: testcli [--verbose]
1571
1572 Flags:
1573 --verbose Enable verbose output
1574 -h, --help Print help
1575 -V, --version Print version
1576 ");
1577 }
1578
1579 #[test]
1580 fn test_render_help_omits_help_when_disabled() {
1581 let spec = crate::spec! { r#"
1585bin "testcli"
1586version "1.2.3"
1587disable_help #true
1588flag "--verbose" help="Enable verbose output"
1589 "# }
1590 .unwrap();
1591
1592 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1593 testcli 1.2.3
1594 Usage: testcli [--verbose]
1595
1596 Flags:
1597 --verbose Enable verbose output
1598 -V, --version Print version
1599 ");
1600 }
1601
1602 #[test]
1603 fn test_render_help_with_author_license() {
1604 let spec = crate::spec! { r#"
1605bin "testcli"
1606author "Test Author"
1607license "MIT"
1608flag "--verbose" help="Enable verbose output"
1609 "# }
1610 .unwrap();
1611
1612 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1614 Usage: testcli [--verbose]
1615
1616 Flags:
1617 --verbose Enable verbose output
1618 -h, --help Print help
1619 ");
1620
1621 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1623 Usage: testcli [--verbose]
1624
1625 Flags:
1626 --verbose Enable verbose output
1627 -h, --help Print help
1628
1629 Author: Test Author
1630 License: MIT
1631 ");
1632 }
1633
1634 #[test]
1635 fn test_render_help_with_deprecated_command() {
1636 let spec = crate::spec! { r#"
1637bin "testcli"
1638flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
1639cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
1640cmd "new-cmd" help="Do something better"
1641 "# }
1642 .unwrap();
1643
1644 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1645 Usage: testcli [--old] <SUBCOMMAND>
1646
1647 Commands:
1648 new-cmd Do something better
1649 old-cmd Do something [deprecated: use new-cmd instead; warns at 6.2; removed
1650 at 7.0]
1651 help Print this message or the help of the given subcommand(s)
1652
1653 Flags:
1654 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
1655 -h, --help Print help
1656 ");
1657 }
1658
1659 #[test]
1660 fn deprecation_milestones_do_not_need_a_message() {
1661 let spec = crate::spec! { r#"
1662bin "testcli"
1663flag "--old" help="Old switch" deprecated_remove_at="7.0"
1664cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
1665 "# }
1666 .unwrap();
1667
1668 let page = render_help(&spec, &spec.cmd, false);
1669 assert!(
1670 page.contains("old-cmd Do something [deprecated: warns at 6.2]"),
1671 "{page}"
1672 );
1673 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
1674 assert!(!page.contains("[deprecated:;"), "{page}");
1675 }
1676
1677 #[test]
1678 fn test_render_help_with_subcommand_presentation() {
1679 let spec = crate::spec! { r#"
1680bin "testcli"
1681subcommand_help_heading "Actions"
1682subcommand_value_name "ACTION"
1683cmd "run" help="Run it\n"
1684 "# }
1685 .unwrap();
1686
1687 let page = render_help(&spec, &spec.cmd, false);
1688 assert!(page.contains("Usage: testcli <ACTION>"), "{page}");
1689 assert!(page.contains("\nActions:\n"), "{page}");
1690 }
1691
1692 #[test]
1693 fn test_render_help_honors_explicit_display_order() {
1694 let spec = crate::spec! { r#"
1695bin "testcli"
1696flag "--unset" help="Unordered"
1697flag "--later" help="Later" display_order=20
1698flag "--first" help="First" display_order=10
1699cmd "zulu" help="Unordered"
1700cmd "later" help="Later" display_order=20
1701cmd "first" help="First" display_order=10
1702cmd "alpha" help="Unordered"
1703 "# }
1704 .unwrap();
1705
1706 let page = render_help(&spec, &spec.cmd, false);
1707 let commands = page.split_once("\nCommands:\n").unwrap().1;
1708 assert!(
1709 commands.find("first").unwrap() < commands.find("later").unwrap()
1710 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
1711 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
1712 "{page}"
1713 );
1714 let flags = page.split_once("\nFlags:\n").unwrap().1;
1715 assert!(
1716 flags.find("--first").unwrap() < flags.find("--later").unwrap()
1717 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
1718 "{page}"
1719 );
1720 }
1721
1722 #[test]
1723 fn test_render_help_groups_subcommands_by_heading() {
1724 let spec = crate::spec! { r#"
1725bin "testcli"
1726cmd "run" help="Run it" help_heading="Core commands"
1727cmd "clean" help="Remove old state" help_heading="Maintenance"
1728cmd "status" help="Show status" help_heading="Commands"
1729 "# }
1730 .unwrap();
1731
1732 for page in [
1733 render_help(&spec, &spec.cmd, false),
1734 render_help(&spec, &spec.cmd, true),
1735 ] {
1736 let commands = page.find("\nCommands:\n").expect("default command section");
1737 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
1738 let core = page.find("\nCore commands:\n").expect("core section");
1739 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
1740 assert!(commands < core && commands < maintenance, "{page}");
1741 let default_end = core.min(maintenance);
1742 assert!(page[commands..default_end].contains("status"), "{page}");
1743 assert!(page[commands..default_end].contains("help"), "{page}");
1744 assert!(page[core..].contains("run"), "{page}");
1745 assert!(page[maintenance..].contains("clean"), "{page}");
1746 }
1747 }
1748
1749 #[test]
1750 fn test_render_help_with_next_line_layout() {
1751 let spec = crate::spec! { r#"
1752bin "testcli"
1753next_line_help #true
1754arg "<input>" help="Input file" env="INPUT" default="fast" {
1755 choices {
1756 choice "fast"
1757 choice "slow"
1758 }
1759}
1760flag "--verbose" help="Enable verbose output"
1761cmd "run" help="Run it"
1762 "# }
1763 .unwrap();
1764
1765 let short = render_help(&spec, &spec.cmd, false);
1766 assert!(!short.contains(" Run it\n\n help"), "{short}");
1767 for page in [short, render_help(&spec, &spec.cmd, true)] {
1768 assert!(page.contains(" [input]\n Input file"), "{page}");
1769 assert!(
1770 page.contains("--verbose\n Enable verbose output"),
1771 "{page}"
1772 );
1773 assert!(
1774 page.contains(
1775 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
1776 ),
1777 "{page}"
1778 );
1779 assert!(page.contains(" run\n Run it"), "{page}");
1780 }
1781 }
1782
1783 #[test]
1784 fn flatten_help_expands_subcommands_instead_of_listing_them() {
1785 let spec = crate::spec! { r#"
1786bin "testcli"
1787flatten_help #true
1788next_line_help #true
1789cmd "run" help="Run it" {
1790 arg "<task>" help="Task name" env="TASK" default="build" {
1791 choices {
1792 choice "build"
1793 choice "test"
1794 }
1795 }
1796 flag "--dry-run" help="Only show changes"
1797 flatten_help #true
1798 cmd "nested" help="Nested operation" {
1799 flag "--deep" help="Deep option"
1800 }
1801}
1802 "# }
1803 .unwrap();
1804
1805 for page in [
1806 render_help(&spec, &spec.cmd, false),
1807 render_help(&spec, &spec.cmd, true),
1808 ] {
1809 assert!(
1810 page.contains("Usage: testcli\n testcli run"),
1811 "{page}"
1812 );
1813 assert!(!page.contains("\nCommands:\n"), "{page}");
1814 assert!(page.contains("\nrun:\nRun it"), "{page}");
1815 assert!(page.contains("[task]"), "{page}");
1816 assert!(page.contains("--dry-run"), "{page}");
1817 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
1818 assert!(page.contains("--deep"), "{page}");
1819 assert!(
1820 page.contains(
1821 " [possible values: build, test]\n [env: TASK]\n (default: build)"
1822 ),
1823 "{page}"
1824 );
1825 }
1826 }
1827}