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