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.clone());
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 items: supplied,
54 },
55 ),
56 }
57 }
58 }
59
60 let width = crate::docs::layout::help_width(cmd.term_width, cmd.max_term_width);
61 let col = crate::docs::layout::max_usage_width(
62 docs_cmd
63 .flag_groups
64 .iter()
65 .flat_map(|g| g.items.iter())
66 .chain(inherited.iter())
67 .map(|f| f.display_usage.as_str()),
68 );
69 for group in &mut docs_cmd.flag_groups {
70 lay_out(&mut group.items, width, col);
71 }
72 lay_out(&mut inherited, width, col);
73
74 ctx.insert("cmd", &docs_cmd);
77 ctx.insert("global_flags", &inherited);
78 for (name, mark) in MARKS {
79 ctx.insert(name, &mark);
80 }
81 let template = if long {
82 "spec_template_long.tera"
83 } else {
84 "spec_template_short.tera"
85 };
86 let rendered = TERA.render(template, &ctx).unwrap();
87 let sections = Sections::split(&rendered);
88 let page = match spec
89 .help_template
90 .as_deref()
91 .filter(|t| crate::help_template::is_set(t))
92 {
93 Some(template) => crate::help_template::substitute(template, |name| sections.named(name)),
94 None => sections.concatenated(),
95 };
96 page.trim().to_string() + "\n"
97}
98
99const MARKS: [(&str, &str); 6] = [
110 ("mark_usage", "\u{1}usage\u{1}"),
111 ("mark_commands", "\u{1}commands\u{1}"),
112 ("mark_args", "\u{1}args\u{1}"),
113 ("mark_flags", "\u{1}flags\u{1}"),
114 ("mark_flattened", "\u{1}flattened\u{1}"),
115 ("mark_after_help", "\u{1}after_help\u{1}"),
116];
117
118struct Sections<'a> {
124 about: &'a str,
125 usage: &'a str,
126 commands: &'a str,
127 args: &'a str,
128 flags: &'a str,
129 flattened: &'a str,
130 after_help: &'a str,
131}
132
133impl<'a> Sections<'a> {
134 fn split(rendered: &'a str) -> Self {
135 let mut rest = rendered;
136 let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
137 for (_, mark) in MARKS {
138 match rest.split_once(mark) {
142 Some((before, after)) => {
143 parts.push(before);
144 rest = after;
145 }
146 None => parts.push(""),
147 }
148 }
149 parts.push(rest);
150 Self {
151 about: parts[0],
152 usage: parts[1],
153 commands: parts[2],
154 args: parts[3],
155 flags: parts[4],
156 flattened: parts[5],
157 after_help: parts[6],
158 }
159 }
160
161 fn concatenated(&self) -> String {
163 [
164 self.about,
165 self.usage,
166 self.commands,
167 self.args,
168 self.flags,
169 self.flattened,
170 self.after_help,
171 ]
172 .concat()
173 }
174
175 fn named(&self, name: &str) -> Option<String> {
177 Some(match name {
178 "about" => self.about.trim().to_string(),
179 "usage" => self.usage.trim().to_string(),
180 "commands" => {
181 let mut out = self.commands.trim().to_string();
182 let flattened = self.flattened.trim();
183 if !flattened.is_empty() {
184 if !out.is_empty() {
185 out.push_str("\n\n");
186 }
187 out.push_str(flattened);
188 }
189 out
190 }
191 "args" => self.args.trim().to_string(),
192 "flags" => self.flags.trim().to_string(),
193 "after_help" => self.after_help.trim().to_string(),
194 _ => return None,
195 })
196 }
197}
198
199fn supplied_flags(
213 spec: &Spec,
214 cmd: &SpecCommand,
215 ancestors_taken: &[String],
216 is_root: bool,
217) -> Vec<crate::docs::models::SpecFlag> {
218 let mut taken: Vec<String> = ancestors_taken.to_vec();
223 for f in &cmd.flags {
224 taken.extend(f.long.iter().map(|l| format!("--{l}")));
225 taken.extend(f.short.iter().map(|s| format!("-{s}")));
226 taken.extend(f.negate.clone());
228 }
229
230 let build = |name: &str, long: &str, short: char, help: &str| {
231 let long_free = !taken.contains(&format!("--{long}"));
232 let short_free = !taken.contains(&format!("-{short}"));
233 if !long_free && !short_free {
234 return None;
235 }
236 let name = if long_free { name } else { &short.to_string() };
239 let mut flag = crate::SpecFlag {
240 name: name.to_string(),
241 long: if long_free {
242 vec![long.to_string()]
243 } else {
244 vec![]
245 },
246 short: if short_free { vec![short] } else { vec![] },
247 help: Some(help.to_string()),
248 ..Default::default()
249 };
250 flag.usage = flag.usage();
251 Some(crate::docs::models::SpecFlag::from(&flag))
252 };
253
254 let mut out = Vec::new();
255 if spec.disable_help != Some(true) && !cmd.disable_help_flag {
262 out.extend(build("help", "help", 'h', "Print help"));
263 }
264 if is_root
265 && (spec.version.is_some() || spec.long_version.is_some())
266 && !cmd.disable_version_flag
267 {
268 out.extend(build("version", "version", 'V', "Print version"));
269 }
270 out
271}
272
273fn lay_out(flags: &mut [crate::docs::models::SpecFlag], terminal_width: usize, col: usize) {
280 for flag in flags {
281 flag.usage_col_width = col;
282 flag.help_rendered = None;
283 flag.help_is_multiline = false;
284 let help = flag.help_long.as_deref().or(flag.help.as_deref());
285 if let Some(help) = help {
286 let (rendered, is_multiline) =
287 crate::docs::layout::render_help_text(help, terminal_width, col);
288 if !rendered.is_empty() {
290 flag.help_rendered = Some(rendered);
291 flag.help_is_multiline = is_multiline;
292 }
293 }
294 }
295}
296
297fn inherited_flags(
306 spec: &Spec,
307 cmd: &SpecCommand,
308 full_cmd: &[String],
309 long_help: bool,
310) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
311 let mut ancestors: Vec<&SpecCommand> = Vec::new();
313 let mut at = &spec.cmd;
314 for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
315 ancestors.push(at);
316 let Some(next) = at.subcommands.get(name) else {
317 return (Vec::new(), Vec::new());
318 };
319 at = next;
320 }
321 if !full_cmd.is_empty() {
322 ancestors.push(at);
323 }
324
325 let forms = |f: &crate::SpecFlag| -> Vec<String> {
338 f.long
339 .iter()
340 .map(|l| format!("--{l}"))
341 .chain(f.short.iter().map(|s| format!("-{s}")))
342 .collect()
343 };
344 let every_form: Vec<String> = cmd
345 .flags
346 .iter()
347 .chain(
348 ancestors
349 .iter()
350 .flat_map(|a| a.flags.iter())
351 .filter(|f| f.global),
352 )
353 .flat_map(&forms)
354 .collect();
355
356 let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
357 let mut taken_negations: Vec<String> =
358 cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
359 let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
360 for ancestor in ancestors.iter().rev() {
361 for f in ancestor.flags.iter().filter(|f| f.global) {
362 let long = f
363 .long
364 .iter()
365 .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
366 .cloned();
367 let short = f
368 .short
369 .iter()
370 .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
371 .copied();
372 let mine = forms(f);
373 let negate = f.negate.as_ref().is_some_and(|n| {
374 !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
375 });
376 taken.extend(forms(f));
379 taken_negations.extend(f.negate.clone());
380 if f.hide
381 || if long_help {
382 f.hide_long_help
383 } else {
384 f.hide_short_help
385 }
386 || (long.is_none() && short.is_none() && !negate)
387 {
388 continue;
389 }
390 keep.push((f, long, short, negate));
391 }
392 }
393 let shown: Vec<crate::docs::models::SpecFlag> = ancestors
394 .iter()
395 .flat_map(|a| a.flags.iter())
396 .filter_map(|f| {
397 keep.iter()
398 .find(|(k, _, _, _)| std::ptr::eq(*k, f))
399 .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
400 })
401 .map(|(f, long, short, negate)| {
402 let mut shown = f.clone();
405 shown.long = long.into_iter().collect();
406 shown.short = short.into_iter().collect();
407 if !negate {
408 shown.negate = None;
409 }
410 shown.usage = shown.usage();
411 crate::docs::models::SpecFlag::from(&shown)
412 })
413 .collect();
414 taken.extend(taken_negations);
418 (shown, taken)
419}
420
421fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
433 let mut visible = cmd.clone();
434 visible.flags.retain(|flag| {
435 !flag.hide
436 && if long {
437 !flag.hide_long_help
438 } else {
439 !flag.hide_short_help
440 }
441 });
442 visible.args.retain(|arg| {
443 !arg.hide
444 && if long {
445 !arg.hide_long_help
446 } else {
447 !arg.hide_short_help
448 }
449 });
450 visible.subcommands.retain(|_, sub| !sub.hide);
451 if visible.flatten_help {
457 for sub in visible.subcommands.values_mut() {
458 *sub = without_hidden(sub, long);
459 }
460 }
461 visible
462}
463
464static TERA: LazyLock<Tera> = LazyLock::new(|| {
465 let mut tera = Tera::default();
466
467 tera.register_filter(
469 "ljust",
470 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
471 let value = value.as_str().unwrap_or("");
472 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
473 Ok(format!("{:<width$}", value, width = width))
474 },
475 );
476 tera.register_filter(
477 "default",
478 |value: &tera::Value,
479 kwargs: tera::Kwargs,
480 _: &tera::State|
481 -> tera::TeraResult<tera::Value> {
482 let default_val = kwargs.must_get::<tera::Value>("value")?;
483 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
484 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
485 Ok(default_val)
486 } else {
487 Ok(value.clone())
488 }
489 },
490 );
491
492 #[rustfmt::skip]
493 tera.add_raw_templates([
494 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
495 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
496 ]).unwrap();
497
498 tera
499});
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504 use insta::assert_snapshot;
505
506 #[test]
507 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
508 let spec = crate::spec! { r#"
513bin "ex"
514flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
515cmd inner help="a command" {
516 flag "--plain" help="its own"
517}
518 "# }
519 .unwrap();
520
521 let inner = spec.cmd.subcommands.get("inner").expect("inner");
522 for long in [false, true] {
523 let page = super::render_help(&spec, inner, long);
524 assert!(
525 !page.contains("--help"),
526 "long={long}: a hidden ancestor binds this:\n{page}"
527 );
528 assert!(page.contains("-h"), "long={long}:\n{page}");
530 }
531 }
532
533 #[test]
534 fn a_long_beats_a_negation_however_far_away_it_is() {
535 let spec = crate::spec! { r#"
541bin "ex"
542flag "--no-cache" global=#true help="the root's plain long"
543flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
544cmd narrow help="a command" {
545 flag "--cache" negate="--no-cache" help="its own, with a negation"
546 flag "--tint" negate="--no-colour" help="claims the root's negation"
547}
548 "# }
549 .unwrap();
550
551 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
552 for long in [false, true] {
553 let page = super::render_help(&spec, narrow, long);
554 assert!(
555 page.contains("--no-cache"),
556 "long={long}: a long beats a negation, so this still binds here:\n{page}"
557 );
558 assert!(page.contains("--colour"), "long={long}:\n{page}");
561 let global = page
562 .split_once("Global flags:")
563 .expect("a global section")
564 .1;
565 assert!(
566 !global.contains("--colour / --no-colour"),
567 "long={long}: the nearer negation owns that spelling:\n{page}"
568 );
569 }
570 }
571
572 #[test]
573 fn a_description_of_only_spaces_is_no_description() {
574 let spec = crate::spec! { r#"
581bin "ex"
582flag "--blank" help=" "
583flag "--plain" help="plain"
584 "# }
585 .unwrap();
586
587 for long in [false, true] {
588 let page = super::render_help(&spec, &spec.cmd, long);
589 let listing = page.split_once("\nFlags:").expect("a flags section").1;
592 let line = listing
593 .lines()
594 .find(|l| l.contains("--blank"))
595 .unwrap_or_else(|| panic!("long={long}: {page}"));
596 assert_eq!(
597 line,
598 line.trim_end(),
599 "long={long}: trailing space on {line:?}"
600 );
601 }
602 }
603
604 #[test]
605 fn test_render_help_omits_hidden_entries() {
606 let spec = crate::spec! { r#"
607bin "ex"
608flag "--visible" help="shown"
609flag "--secret" hide=#true help="hidden"
610flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
611arg "[SHOWN]" help="an arg"
612arg "[HIDDEN]" hide=#true help="a hidden arg"
613cmd open help="a command"
614cmd sneaky hide=#true help="a hidden command"
615 "# }
616 .unwrap();
617
618 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
623 Usage: ex [--visible] [SHOWN] <SUBCOMMAND>
624
625 Commands:
626 open a command
627 help Print this message or the help of the given subcommand(s)
628
629 Arguments:
630 [SHOWN] an arg
631
632 Flags:
633 --visible shown
634 -h, --help Print help
635 ");
636 }
637
638 #[test]
639 fn test_render_help_groups_by_heading() {
640 let spec = crate::spec! { r#"
641bin "testcli"
642flag "--verbose" help="Verbose output"
643flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
644flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
645flag "--jobs <n>" help="How many at once" help_heading="Performance"
646arg "<file>" help="The file"
647arg "<mode>" help="How to run" help_heading="Behaviour"
648 "# }
649 .unwrap();
650
651 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
654 Usage: testcli [FLAGS] <file> <mode>
655
656 Arguments:
657 <file> The file
658
659 Behaviour:
660 <mode> How to run
661
662 Flags:
663 --verbose Verbose output
664 -h, --help Print help
665
666 Filtering:
667 --filter <pattern> Only matching
668 --exclude <pattern> Skip matching
669
670 Performance:
671 --jobs <n> How many at once
672 ");
673 }
674
675 #[test]
676 fn test_render_help_with_only_headed_flags() {
677 let spec = crate::spec! { r#"
680bin "testcli"
681flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
682 "# }
683 .unwrap();
684
685 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
686 Usage: testcli [--filter <pattern>]
687
688 Flags:
689 -h, --help Print help
690
691 Filtering:
692 --filter <pattern> Only matching
693 ");
694 }
695
696 #[test]
697 fn test_render_help_with_env() {
698 let spec = crate::spec! { r#"
699bin "testcli"
700flag "--color" env="MYCLI_COLOR" help="Enable color output"
701flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
702flag "--debug" help="Debug mode"
703 "# }
704 .unwrap();
705
706 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
707 Usage: testcli [FLAGS]
708
709 Flags:
710 --color Enable color output [env: MYCLI_COLOR]
711 --verbose Verbose output [env: MYCLI_VERBOSE]
712 --debug Debug mode
713 -h, --help Print help
714 ");
715
716 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
717 Usage: testcli [FLAGS]
718
719 Flags:
720 --color Enable color output
721 [env: MYCLI_COLOR]
722 --verbose Verbose output
723 [env: MYCLI_VERBOSE]
724 --debug Debug mode
725 -h, --help Print help
726 ");
727 }
728
729 #[test]
730 fn test_render_help_with_arg_env() {
731 let spec = crate::spec! { r#"
732bin "testcli"
733arg "<input>" env="MY_INPUT" help="Input file"
734arg "<output>" env="MY_OUTPUT" help="Output file"
735arg "<extra>" help="Extra arg without env"
736arg "[default]" help="Arg with default value" default="default value"
737 "# }
738 .unwrap();
739
740 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
741 Usage: testcli <ARGS>…
742
743 Arguments:
744 <input> Input file [env: MY_INPUT]
745 <output> Output file [env: MY_OUTPUT]
746 <extra> Extra arg without env
747 [default] Arg with default value (default: default value)
748
749 Flags:
750 -h, --help Print help
751 ");
752
753 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
754 Usage: testcli <ARGS>…
755
756 Arguments:
757 <input> Input file
758 [env: MY_INPUT]
759 <output> Output file
760 [env: MY_OUTPUT]
761 <extra> Extra arg without env
762 [default] Arg with default value
763 (default: default value)
764
765 Flags:
766 -h, --help Print help
767 ");
768 }
769
770 #[test]
771 fn test_render_help_with_negated_flag() {
772 let spec = crate::spec! { r#"
773bin "testcli"
774flag "--compress" negate="--no-compress" default=#true help="Compress output"
775flag "--verbose" help="Verbose output"
776 "# }
777 .unwrap();
778
779 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
780 Usage: testcli [--compress] [--verbose]
781
782 Flags:
783 --compress / --no-compress Compress output (default: true)
784 --verbose Verbose output
785 -h, --help Print help
786 ");
787
788 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
789 Usage: testcli [--compress] [--verbose]
790
791 Flags:
792 --compress / --no-compress Compress output
793 (default: true)
794 --verbose Verbose output
795 -h, --help Print help
796 ");
797 }
798
799 #[test]
800 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
801 let spec = crate::spec! { r#"
802bin "testcli"
803flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
804 choices {
805 choice "fast"
806 choice "slow"
807 }
808}
809flag "--short-only" help="short" hide_long_help=#true
810flag "--long-only" help="long" hide_short_help=#true
811arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
812 "# }
813 .unwrap();
814
815 let short = render_help(&spec, &spec.cmd, false);
816 assert!(short.contains("--mode <mode>"), "{short}");
817 assert!(short.contains("--short-only"), "{short}");
818 assert!(!short.contains("--long-only"), "{short}");
819 assert!(
820 !short.contains("MODE") && !short.contains("fast, slow"),
821 "{short}"
822 );
823 assert!(
824 !short.contains("default: fast") && !short.contains("default: file"),
825 "{short}"
826 );
827
828 let long = render_help(&spec, &spec.cmd, true);
829 assert!(long.contains("--long-only"), "{long}");
830 assert!(!long.contains("--short-only"), "{long}");
831 assert!(
832 !long.contains("MODE") && !long.contains("possible values"),
833 "{long}"
834 );
835
836 let rendered = spec.to_string();
837 let reparsed: crate::Spec = rendered.parse().unwrap();
838 assert!(reparsed.cmd.flags[0].hide_default_value);
839 assert!(reparsed.cmd.flags[0].hide_env);
840 assert!(reparsed.cmd.flags[0].hide_possible_values);
841 }
842
843 #[test]
844 fn a_help_template_reorders_omits_and_wraps_the_sections() {
845 let spec = crate::spec! { r#"
849bin "ex"
850about "An example"
851help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
852flag "--force" help="Do it anyway"
853arg "<file>" help="Which file"
854cmd "run" help="Run it"
855 "# }
856 .unwrap();
857
858 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
859 An example
860
861 Usage: ex [--force] <file> <SUBCOMMAND>
862
863 Flags:
864 --force Do it anyway
865 -h, --help Print help
866
867 Arguments:
868 <file> Which file
869
870 -- ask a person --
871 ");
872 }
873
874 #[test]
875 fn a_template_places_the_sections_a_page_actually_has() {
876 let spec = crate::spec! { r#"
882bin "ex"
883version "1.2.3"
884about "An example"
885after_help "Read the docs."
886help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
887flag "--force" help="Do it anyway"
888cmd "run" help="Run it"
889 "# }
890 .unwrap();
891
892 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
893 Usage: ex [--force] <SUBCOMMAND>
894
895 Flags:
896 --force Do it anyway
897 -h, --help Print help
898 -V, --version Print version
899
900 Commands:
901 run
902 Run it
903
904 help
905 Print this message or the help of the given subcommand(s)
906
907 Read the docs.
908
909 ex 1.2.3
910 An example
911 ");
912 }
913
914 #[test]
915 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
916 let spec = crate::spec! { r#"
919bin "ex"
920flatten_help #true
921help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
922cmd "run" help="Run it" {
923 flag "--dry-run" help="Only show changes"
924}
925 "# }
926 .unwrap();
927
928 let page = render_help(&spec, &spec.cmd, false);
929 assert!(
930 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
931 "{page}"
932 );
933 assert!(page.contains("--dry-run"), "{page}");
934 }
935
936 #[test]
937 fn test_render_help_with_before_after_help() {
938 let spec = crate::spec! { r#"
939bin "testcli"
940before_help "This text appears before the help"
941after_help "This text appears after the help"
942flag "--verbose" help="Enable verbose output"
943 "# }
944 .unwrap();
945
946 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
947 This text appears before the help
948
949 Usage: testcli [--verbose]
950
951 Flags:
952 --verbose Enable verbose output
953 -h, --help Print help
954
955 This text appears after the help
956 ");
957 }
958
959 #[test]
960 fn test_render_help_with_before_after_help_long() {
961 let spec = crate::spec! { r#"
962bin "testcli"
963before_help "short before"
964before_help_long "This is the long version of before help"
965after_help "short after"
966after_help_long "This is the long version of after help"
967flag "--verbose" help="Enable verbose output"
968 "# }
969 .unwrap();
970
971 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
972 short before
973
974 Usage: testcli [--verbose]
975
976 Flags:
977 --verbose Enable verbose output
978 -h, --help Print help
979
980 short after
981 ");
982
983 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
984 This is the long version of before help
985
986 Usage: testcli [--verbose]
987
988 Flags:
989 --verbose Enable verbose output
990 -h, --help Print help
991
992 This is the long version of after help
993 ");
994 }
995
996 #[test]
997 fn test_render_help_with_examples() {
998 let spec = crate::spec! { r#"
999bin "testcli"
1000flag "--verbose" help="Enable verbose output"
1001example "testcli --verbose" header="Run with verbose output"
1002example "testcli" header="Run normally" help="Just runs the tool"
1003 "# }
1004 .unwrap();
1005
1006 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1007 Usage: testcli [--verbose]
1008
1009 Flags:
1010 --verbose Enable verbose output
1011 -h, --help Print help
1012
1013 Examples:
1014 Run with verbose output:
1015 $ testcli --verbose
1016 Run normally:
1017 $ testcli
1018 ");
1019
1020 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1021 Usage: testcli [--verbose]
1022
1023 Flags:
1024 --verbose Enable verbose output
1025 -h, --help Print help
1026
1027 Examples:
1028 Run with verbose output:
1029 $ testcli --verbose
1030 Run normally:
1031 Just runs the tool
1032 $ testcli
1033 ");
1034 }
1035
1036 #[test]
1037 fn test_render_help_with_version() {
1038 let spec = crate::spec! { r#"
1039bin "testcli"
1040name "TestCLI"
1041version "1.2.3"
1042flag "--verbose" help="Enable verbose output"
1043 "# }
1044 .unwrap();
1045
1046 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1047 TestCLI 1.2.3
1048 Usage: testcli [--verbose]
1049
1050 Flags:
1051 --verbose Enable verbose output
1052 -h, --help Print help
1053 -V, --version Print version
1054 ");
1055 }
1056
1057 #[test]
1058 fn test_render_help_with_only_long_version() {
1059 let spec = crate::spec! { r#"
1060bin "testcli"
1061long_version "1.2.3\ncommit abc123"
1062flag "--verbose" help="Enable verbose output"
1063 "# }
1064 .unwrap();
1065
1066 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1067 Usage: testcli [--verbose]
1068
1069 Flags:
1070 --verbose Enable verbose output
1071 -h, --help Print help
1072 -V, --version Print version
1073 ");
1074 }
1075
1076 #[test]
1077 fn test_render_help_omits_help_when_disabled() {
1078 let spec = crate::spec! { r#"
1082bin "testcli"
1083version "1.2.3"
1084disable_help #true
1085flag "--verbose" help="Enable verbose output"
1086 "# }
1087 .unwrap();
1088
1089 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1090 testcli 1.2.3
1091 Usage: testcli [--verbose]
1092
1093 Flags:
1094 --verbose Enable verbose output
1095 -V, --version Print version
1096 ");
1097 }
1098
1099 #[test]
1100 fn test_render_help_with_author_license() {
1101 let spec = crate::spec! { r#"
1102bin "testcli"
1103author "Test Author"
1104license "MIT"
1105flag "--verbose" help="Enable verbose output"
1106 "# }
1107 .unwrap();
1108
1109 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1111 Usage: testcli [--verbose]
1112
1113 Flags:
1114 --verbose Enable verbose output
1115 -h, --help Print help
1116 ");
1117
1118 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1120 Usage: testcli [--verbose]
1121
1122 Flags:
1123 --verbose Enable verbose output
1124 -h, --help Print help
1125
1126 Author: Test Author
1127 License: MIT
1128 ");
1129 }
1130
1131 #[test]
1132 fn test_render_help_with_deprecated_command() {
1133 let spec = crate::spec! { r#"
1134bin "testcli"
1135flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
1136cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
1137cmd "new-cmd" help="Do something better"
1138 "# }
1139 .unwrap();
1140
1141 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1142 Usage: testcli [--old] <SUBCOMMAND>
1143
1144 Commands:
1145 new-cmd Do something better
1146 old-cmd [deprecated: use new-cmd instead; warns at 6.2; removed at 7.0] Do something
1147 help Print this message or the help of the given subcommand(s)
1148
1149 Flags:
1150 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
1151 -h, --help Print help
1152 ");
1153 }
1154
1155 #[test]
1156 fn deprecation_milestones_do_not_need_a_message() {
1157 let spec = crate::spec! { r#"
1158bin "testcli"
1159flag "--old" help="Old switch" deprecated_remove_at="7.0"
1160cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
1161 "# }
1162 .unwrap();
1163
1164 let page = render_help(&spec, &spec.cmd, false);
1165 assert!(
1166 page.contains("old-cmd [deprecated: warns at 6.2]"),
1167 "{page}"
1168 );
1169 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
1170 assert!(!page.contains("[deprecated:;"), "{page}");
1171 }
1172
1173 #[test]
1174 fn test_render_help_with_subcommand_presentation() {
1175 let spec = crate::spec! { r#"
1176bin "testcli"
1177subcommand_help_heading "Actions"
1178subcommand_value_name "ACTION"
1179cmd "run" help="Run it\n"
1180 "# }
1181 .unwrap();
1182
1183 let page = render_help(&spec, &spec.cmd, false);
1184 assert!(page.contains("Usage: testcli <ACTION>"), "{page}");
1185 assert!(page.contains("\nActions:\n"), "{page}");
1186 }
1187
1188 #[test]
1189 fn test_render_help_honors_explicit_display_order() {
1190 let spec = crate::spec! { r#"
1191bin "testcli"
1192flag "--unset" help="Unordered"
1193flag "--later" help="Later" display_order=20
1194flag "--first" help="First" display_order=10
1195cmd "zulu" help="Unordered"
1196cmd "later" help="Later" display_order=20
1197cmd "first" help="First" display_order=10
1198cmd "alpha" help="Unordered"
1199 "# }
1200 .unwrap();
1201
1202 let page = render_help(&spec, &spec.cmd, false);
1203 let commands = page.split_once("\nCommands:\n").unwrap().1;
1204 assert!(
1205 commands.find("first").unwrap() < commands.find("later").unwrap()
1206 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
1207 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
1208 "{page}"
1209 );
1210 let flags = page.split_once("\nFlags:\n").unwrap().1;
1211 assert!(
1212 flags.find("--first").unwrap() < flags.find("--later").unwrap()
1213 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
1214 "{page}"
1215 );
1216 }
1217
1218 #[test]
1219 fn test_render_help_groups_subcommands_by_heading() {
1220 let spec = crate::spec! { r#"
1221bin "testcli"
1222cmd "run" help="Run it" help_heading="Core commands"
1223cmd "clean" help="Remove old state" help_heading="Maintenance"
1224cmd "status" help="Show status" help_heading="Commands"
1225 "# }
1226 .unwrap();
1227
1228 for page in [
1229 render_help(&spec, &spec.cmd, false),
1230 render_help(&spec, &spec.cmd, true),
1231 ] {
1232 let commands = page.find("\nCommands:\n").expect("default command section");
1233 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
1234 let core = page.find("\nCore commands:\n").expect("core section");
1235 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
1236 assert!(commands < core && commands < maintenance, "{page}");
1237 let default_end = core.min(maintenance);
1238 assert!(page[commands..default_end].contains("status"), "{page}");
1239 assert!(page[commands..default_end].contains("help"), "{page}");
1240 assert!(page[core..].contains("run"), "{page}");
1241 assert!(page[maintenance..].contains("clean"), "{page}");
1242 }
1243 }
1244
1245 #[test]
1246 fn test_render_help_with_next_line_layout() {
1247 let spec = crate::spec! { r#"
1248bin "testcli"
1249next_line_help #true
1250arg "<input>" help="Input file" env="INPUT" default="fast" {
1251 choices {
1252 choice "fast"
1253 choice "slow"
1254 }
1255}
1256flag "--verbose" help="Enable verbose output"
1257cmd "run" help="Run it"
1258 "# }
1259 .unwrap();
1260
1261 let short = render_help(&spec, &spec.cmd, false);
1262 assert!(!short.contains(" Run it\n\n help"), "{short}");
1263 for page in [short, render_help(&spec, &spec.cmd, true)] {
1264 assert!(page.contains(" [input]\n Input file"), "{page}");
1265 assert!(
1266 page.contains("--verbose\n Enable verbose output"),
1267 "{page}"
1268 );
1269 assert!(
1270 page.contains(
1271 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
1272 ),
1273 "{page}"
1274 );
1275 assert!(page.contains(" run\n Run it"), "{page}");
1276 }
1277 }
1278
1279 #[test]
1280 fn flatten_help_expands_subcommands_instead_of_listing_them() {
1281 let spec = crate::spec! { r#"
1282bin "testcli"
1283flatten_help #true
1284next_line_help #true
1285cmd "run" help="Run it" {
1286 arg "<task>" help="Task name" env="TASK" default="build" {
1287 choices {
1288 choice "build"
1289 choice "test"
1290 }
1291 }
1292 flag "--dry-run" help="Only show changes"
1293 flatten_help #true
1294 cmd "nested" help="Nested operation" {
1295 flag "--deep" help="Deep option"
1296 }
1297}
1298 "# }
1299 .unwrap();
1300
1301 for page in [
1302 render_help(&spec, &spec.cmd, false),
1303 render_help(&spec, &spec.cmd, true),
1304 ] {
1305 assert!(
1306 page.contains("Usage: testcli\n testcli run"),
1307 "{page}"
1308 );
1309 assert!(!page.contains("\nCommands:\n"), "{page}");
1310 assert!(page.contains("\nrun:\nRun it"), "{page}");
1311 assert!(page.contains("[task]"), "{page}");
1312 assert!(page.contains("--dry-run"), "{page}");
1313 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
1314 assert!(page.contains("--deep"), "{page}");
1315 assert!(
1316 page.contains(
1317 " [possible values: build, test]\n [env: TASK]\n (default: build)"
1318 ),
1319 "{page}"
1320 );
1321 }
1322 }
1323}