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 flag_aliases_do_not_leak_into_interactive_help() {
508 let spec = crate::spec! { r#"
509bin "ex"
510flag "-t -f --tail --follow" help="Follow output"
511 "# }
512 .unwrap();
513
514 for long in [false, true] {
515 let page = super::render_help(&spec, &spec.cmd, long);
516 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
517 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
518 }
519 }
520
521 #[test]
522 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
523 let spec = crate::spec! { r#"
528bin "ex"
529flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
530cmd inner help="a command" {
531 flag "--plain" help="its own"
532}
533 "# }
534 .unwrap();
535
536 let inner = spec.cmd.subcommands.get("inner").expect("inner");
537 for long in [false, true] {
538 let page = super::render_help(&spec, inner, long);
539 assert!(
540 !page.contains("--help"),
541 "long={long}: a hidden ancestor binds this:\n{page}"
542 );
543 assert!(page.contains("-h"), "long={long}:\n{page}");
545 }
546 }
547
548 #[test]
549 fn a_long_beats_a_negation_however_far_away_it_is() {
550 let spec = crate::spec! { r#"
556bin "ex"
557flag "--no-cache" global=#true help="the root's plain long"
558flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
559cmd narrow help="a command" {
560 flag "--cache" negate="--no-cache" help="its own, with a negation"
561 flag "--tint" negate="--no-colour" help="claims the root's negation"
562}
563 "# }
564 .unwrap();
565
566 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
567 for long in [false, true] {
568 let page = super::render_help(&spec, narrow, long);
569 assert!(
570 page.contains("--no-cache"),
571 "long={long}: a long beats a negation, so this still binds here:\n{page}"
572 );
573 assert!(page.contains("--colour"), "long={long}:\n{page}");
576 let global = page
577 .split_once("Global flags:")
578 .expect("a global section")
579 .1;
580 assert!(
581 !global.contains("--colour / --no-colour"),
582 "long={long}: the nearer negation owns that spelling:\n{page}"
583 );
584 }
585 }
586
587 #[test]
588 fn a_description_of_only_spaces_is_no_description() {
589 let spec = crate::spec! { r#"
596bin "ex"
597flag "--blank" help=" "
598flag "--plain" help="plain"
599 "# }
600 .unwrap();
601
602 for long in [false, true] {
603 let page = super::render_help(&spec, &spec.cmd, long);
604 let listing = page.split_once("\nFlags:").expect("a flags section").1;
607 let line = listing
608 .lines()
609 .find(|l| l.contains("--blank"))
610 .unwrap_or_else(|| panic!("long={long}: {page}"));
611 assert_eq!(
612 line,
613 line.trim_end(),
614 "long={long}: trailing space on {line:?}"
615 );
616 }
617 }
618
619 #[test]
620 fn test_render_help_omits_hidden_entries() {
621 let spec = crate::spec! { r#"
622bin "ex"
623flag "--visible" help="shown"
624flag "--secret" hide=#true help="hidden"
625flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
626arg "[SHOWN]" help="an arg"
627arg "[HIDDEN]" hide=#true help="a hidden arg"
628cmd open help="a command"
629cmd sneaky hide=#true help="a hidden command"
630 "# }
631 .unwrap();
632
633 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
638 Usage: ex [--visible] [SHOWN] <SUBCOMMAND>
639
640 Commands:
641 open a command
642 help Print this message or the help of the given subcommand(s)
643
644 Arguments:
645 [SHOWN] an arg
646
647 Flags:
648 --visible shown
649 -h, --help Print help
650 ");
651 }
652
653 #[test]
654 fn test_render_help_groups_by_heading() {
655 let spec = crate::spec! { r#"
656bin "testcli"
657flag "--verbose" help="Verbose output"
658flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
659flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
660flag "--jobs <n>" help="How many at once" help_heading="Performance"
661arg "<file>" help="The file"
662arg "<mode>" help="How to run" help_heading="Behaviour"
663 "# }
664 .unwrap();
665
666 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
669 Usage: testcli [FLAGS] <file> <mode>
670
671 Arguments:
672 <file> The file
673
674 Behaviour:
675 <mode> How to run
676
677 Flags:
678 --verbose Verbose output
679 -h, --help Print help
680
681 Filtering:
682 --filter <pattern> Only matching
683 --exclude <pattern> Skip matching
684
685 Performance:
686 --jobs <n> How many at once
687 ");
688 }
689
690 #[test]
691 fn test_render_help_with_only_headed_flags() {
692 let spec = crate::spec! { r#"
695bin "testcli"
696flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
697 "# }
698 .unwrap();
699
700 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
701 Usage: testcli [--filter <pattern>]
702
703 Flags:
704 -h, --help Print help
705
706 Filtering:
707 --filter <pattern> Only matching
708 ");
709 }
710
711 #[test]
712 fn test_render_help_with_env() {
713 let spec = crate::spec! { r#"
714bin "testcli"
715flag "--color" env="MYCLI_COLOR" help="Enable color output"
716flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
717flag "--debug" help="Debug mode"
718 "# }
719 .unwrap();
720
721 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
722 Usage: testcli [FLAGS]
723
724 Flags:
725 --color Enable color output [env: MYCLI_COLOR]
726 --verbose Verbose output [env: MYCLI_VERBOSE]
727 --debug Debug mode
728 -h, --help Print help
729 ");
730
731 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
732 Usage: testcli [FLAGS]
733
734 Flags:
735 --color Enable color output
736 [env: MYCLI_COLOR]
737 --verbose Verbose output
738 [env: MYCLI_VERBOSE]
739 --debug Debug mode
740 -h, --help Print help
741 ");
742 }
743
744 #[test]
745 fn test_render_help_with_arg_env() {
746 let spec = crate::spec! { r#"
747bin "testcli"
748arg "<input>" env="MY_INPUT" help="Input file"
749arg "<output>" env="MY_OUTPUT" help="Output file"
750arg "<extra>" help="Extra arg without env"
751arg "[default]" help="Arg with default value" default="default value"
752 "# }
753 .unwrap();
754
755 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
756 Usage: testcli <ARGS>…
757
758 Arguments:
759 <input> Input file [env: MY_INPUT]
760 <output> Output file [env: MY_OUTPUT]
761 <extra> Extra arg without env
762 [default] Arg with default value (default: default value)
763
764 Flags:
765 -h, --help Print help
766 ");
767
768 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
769 Usage: testcli <ARGS>…
770
771 Arguments:
772 <input> Input file
773 [env: MY_INPUT]
774 <output> Output file
775 [env: MY_OUTPUT]
776 <extra> Extra arg without env
777 [default] Arg with default value
778 (default: default value)
779
780 Flags:
781 -h, --help Print help
782 ");
783 }
784
785 #[test]
786 fn test_render_help_with_negated_flag() {
787 let spec = crate::spec! { r#"
788bin "testcli"
789flag "--compress" negate="--no-compress" default=#true help="Compress output"
790flag "--verbose" help="Verbose output"
791 "# }
792 .unwrap();
793
794 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
795 Usage: testcli [--compress] [--verbose]
796
797 Flags:
798 --compress / --no-compress Compress output (default: true)
799 --verbose Verbose output
800 -h, --help Print help
801 ");
802
803 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
804 Usage: testcli [--compress] [--verbose]
805
806 Flags:
807 --compress / --no-compress Compress output
808 (default: true)
809 --verbose Verbose output
810 -h, --help Print help
811 ");
812 }
813
814 #[test]
815 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
816 let spec = crate::spec! { r#"
817bin "testcli"
818flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
819 choices {
820 choice "fast"
821 choice "slow"
822 }
823}
824flag "--short-only" help="short" hide_long_help=#true
825flag "--long-only" help="long" hide_short_help=#true
826arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
827 "# }
828 .unwrap();
829
830 let short = render_help(&spec, &spec.cmd, false);
831 assert!(short.contains("--mode <mode>"), "{short}");
832 assert!(short.contains("--short-only"), "{short}");
833 assert!(!short.contains("--long-only"), "{short}");
834 assert!(
835 !short.contains("MODE") && !short.contains("fast, slow"),
836 "{short}"
837 );
838 assert!(
839 !short.contains("default: fast") && !short.contains("default: file"),
840 "{short}"
841 );
842
843 let long = render_help(&spec, &spec.cmd, true);
844 assert!(long.contains("--long-only"), "{long}");
845 assert!(!long.contains("--short-only"), "{long}");
846 assert!(
847 !long.contains("MODE") && !long.contains("possible values"),
848 "{long}"
849 );
850
851 let rendered = spec.to_string();
852 let reparsed: crate::Spec = rendered.parse().unwrap();
853 assert!(reparsed.cmd.flags[0].hide_default_value);
854 assert!(reparsed.cmd.flags[0].hide_env);
855 assert!(reparsed.cmd.flags[0].hide_possible_values);
856 }
857
858 #[test]
859 fn a_help_template_reorders_omits_and_wraps_the_sections() {
860 let spec = crate::spec! { r#"
864bin "ex"
865about "An example"
866help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
867flag "--force" help="Do it anyway"
868arg "<file>" help="Which file"
869cmd "run" help="Run it"
870 "# }
871 .unwrap();
872
873 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
874 An example
875
876 Usage: ex [--force] <file> <SUBCOMMAND>
877
878 Flags:
879 --force Do it anyway
880 -h, --help Print help
881
882 Arguments:
883 <file> Which file
884
885 -- ask a person --
886 ");
887 }
888
889 #[test]
890 fn a_template_places_the_sections_a_page_actually_has() {
891 let spec = crate::spec! { r#"
897bin "ex"
898version "1.2.3"
899about "An example"
900after_help "Read the docs."
901help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
902flag "--force" help="Do it anyway"
903cmd "run" help="Run it"
904 "# }
905 .unwrap();
906
907 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
908 Usage: ex [--force] <SUBCOMMAND>
909
910 Flags:
911 --force Do it anyway
912 -h, --help Print help
913 -V, --version Print version
914
915 Commands:
916 run
917 Run it
918
919 help
920 Print this message or the help of the given subcommand(s)
921
922 Read the docs.
923
924 ex 1.2.3
925 An example
926 ");
927 }
928
929 #[test]
930 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
931 let spec = crate::spec! { r#"
934bin "ex"
935flatten_help #true
936help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
937cmd "run" help="Run it" {
938 flag "--dry-run" help="Only show changes"
939}
940 "# }
941 .unwrap();
942
943 let page = render_help(&spec, &spec.cmd, false);
944 assert!(
945 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
946 "{page}"
947 );
948 assert!(page.contains("--dry-run"), "{page}");
949 }
950
951 #[test]
952 fn test_render_help_with_before_after_help() {
953 let spec = crate::spec! { r#"
954bin "testcli"
955before_help "This text appears before the help"
956after_help "This text appears after the help"
957flag "--verbose" help="Enable verbose output"
958 "# }
959 .unwrap();
960
961 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
962 This text appears before the help
963
964 Usage: testcli [--verbose]
965
966 Flags:
967 --verbose Enable verbose output
968 -h, --help Print help
969
970 This text appears after the help
971 ");
972 }
973
974 #[test]
975 fn test_render_help_with_before_after_help_long() {
976 let spec = crate::spec! { r#"
977bin "testcli"
978before_help "short before"
979before_help_long "This is the long version of before help"
980after_help "short after"
981after_help_long "This is the long version of after help"
982flag "--verbose" help="Enable verbose output"
983 "# }
984 .unwrap();
985
986 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
987 short before
988
989 Usage: testcli [--verbose]
990
991 Flags:
992 --verbose Enable verbose output
993 -h, --help Print help
994
995 short after
996 ");
997
998 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
999 This is the long version of before help
1000
1001 Usage: testcli [--verbose]
1002
1003 Flags:
1004 --verbose Enable verbose output
1005 -h, --help Print help
1006
1007 This is the long version of after help
1008 ");
1009 }
1010
1011 #[test]
1012 fn test_render_help_with_examples() {
1013 let spec = crate::spec! { r#"
1014bin "testcli"
1015flag "--verbose" help="Enable verbose output"
1016example "testcli --verbose" header="Run with verbose output"
1017example "testcli" header="Run normally" help="Just runs the tool"
1018 "# }
1019 .unwrap();
1020
1021 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1022 Usage: testcli [--verbose]
1023
1024 Flags:
1025 --verbose Enable verbose output
1026 -h, --help Print help
1027
1028 Examples:
1029 Run with verbose output:
1030 $ testcli --verbose
1031 Run normally:
1032 $ testcli
1033 ");
1034
1035 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1036 Usage: testcli [--verbose]
1037
1038 Flags:
1039 --verbose Enable verbose output
1040 -h, --help Print help
1041
1042 Examples:
1043 Run with verbose output:
1044 $ testcli --verbose
1045 Run normally:
1046 Just runs the tool
1047 $ testcli
1048 ");
1049 }
1050
1051 #[test]
1052 fn test_render_help_with_version() {
1053 let spec = crate::spec! { r#"
1054bin "testcli"
1055name "TestCLI"
1056version "1.2.3"
1057flag "--verbose" help="Enable verbose output"
1058 "# }
1059 .unwrap();
1060
1061 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1062 TestCLI 1.2.3
1063 Usage: testcli [--verbose]
1064
1065 Flags:
1066 --verbose Enable verbose output
1067 -h, --help Print help
1068 -V, --version Print version
1069 ");
1070 }
1071
1072 #[test]
1073 fn test_render_help_with_only_long_version() {
1074 let spec = crate::spec! { r#"
1075bin "testcli"
1076long_version "1.2.3\ncommit abc123"
1077flag "--verbose" help="Enable verbose output"
1078 "# }
1079 .unwrap();
1080
1081 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1082 Usage: testcli [--verbose]
1083
1084 Flags:
1085 --verbose Enable verbose output
1086 -h, --help Print help
1087 -V, --version Print version
1088 ");
1089 }
1090
1091 #[test]
1092 fn test_render_help_omits_help_when_disabled() {
1093 let spec = crate::spec! { r#"
1097bin "testcli"
1098version "1.2.3"
1099disable_help #true
1100flag "--verbose" help="Enable verbose output"
1101 "# }
1102 .unwrap();
1103
1104 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1105 testcli 1.2.3
1106 Usage: testcli [--verbose]
1107
1108 Flags:
1109 --verbose Enable verbose output
1110 -V, --version Print version
1111 ");
1112 }
1113
1114 #[test]
1115 fn test_render_help_with_author_license() {
1116 let spec = crate::spec! { r#"
1117bin "testcli"
1118author "Test Author"
1119license "MIT"
1120flag "--verbose" help="Enable verbose output"
1121 "# }
1122 .unwrap();
1123
1124 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1126 Usage: testcli [--verbose]
1127
1128 Flags:
1129 --verbose Enable verbose output
1130 -h, --help Print help
1131 ");
1132
1133 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1135 Usage: testcli [--verbose]
1136
1137 Flags:
1138 --verbose Enable verbose output
1139 -h, --help Print help
1140
1141 Author: Test Author
1142 License: MIT
1143 ");
1144 }
1145
1146 #[test]
1147 fn test_render_help_with_deprecated_command() {
1148 let spec = crate::spec! { r#"
1149bin "testcli"
1150flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
1151cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
1152cmd "new-cmd" help="Do something better"
1153 "# }
1154 .unwrap();
1155
1156 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1157 Usage: testcli [--old] <SUBCOMMAND>
1158
1159 Commands:
1160 new-cmd Do something better
1161 old-cmd [deprecated: use new-cmd instead; warns at 6.2; removed at 7.0] Do something
1162 help Print this message or the help of the given subcommand(s)
1163
1164 Flags:
1165 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
1166 -h, --help Print help
1167 ");
1168 }
1169
1170 #[test]
1171 fn deprecation_milestones_do_not_need_a_message() {
1172 let spec = crate::spec! { r#"
1173bin "testcli"
1174flag "--old" help="Old switch" deprecated_remove_at="7.0"
1175cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
1176 "# }
1177 .unwrap();
1178
1179 let page = render_help(&spec, &spec.cmd, false);
1180 assert!(
1181 page.contains("old-cmd [deprecated: warns at 6.2]"),
1182 "{page}"
1183 );
1184 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
1185 assert!(!page.contains("[deprecated:;"), "{page}");
1186 }
1187
1188 #[test]
1189 fn test_render_help_with_subcommand_presentation() {
1190 let spec = crate::spec! { r#"
1191bin "testcli"
1192subcommand_help_heading "Actions"
1193subcommand_value_name "ACTION"
1194cmd "run" help="Run it\n"
1195 "# }
1196 .unwrap();
1197
1198 let page = render_help(&spec, &spec.cmd, false);
1199 assert!(page.contains("Usage: testcli <ACTION>"), "{page}");
1200 assert!(page.contains("\nActions:\n"), "{page}");
1201 }
1202
1203 #[test]
1204 fn test_render_help_honors_explicit_display_order() {
1205 let spec = crate::spec! { r#"
1206bin "testcli"
1207flag "--unset" help="Unordered"
1208flag "--later" help="Later" display_order=20
1209flag "--first" help="First" display_order=10
1210cmd "zulu" help="Unordered"
1211cmd "later" help="Later" display_order=20
1212cmd "first" help="First" display_order=10
1213cmd "alpha" help="Unordered"
1214 "# }
1215 .unwrap();
1216
1217 let page = render_help(&spec, &spec.cmd, false);
1218 let commands = page.split_once("\nCommands:\n").unwrap().1;
1219 assert!(
1220 commands.find("first").unwrap() < commands.find("later").unwrap()
1221 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
1222 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
1223 "{page}"
1224 );
1225 let flags = page.split_once("\nFlags:\n").unwrap().1;
1226 assert!(
1227 flags.find("--first").unwrap() < flags.find("--later").unwrap()
1228 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
1229 "{page}"
1230 );
1231 }
1232
1233 #[test]
1234 fn test_render_help_groups_subcommands_by_heading() {
1235 let spec = crate::spec! { r#"
1236bin "testcli"
1237cmd "run" help="Run it" help_heading="Core commands"
1238cmd "clean" help="Remove old state" help_heading="Maintenance"
1239cmd "status" help="Show status" help_heading="Commands"
1240 "# }
1241 .unwrap();
1242
1243 for page in [
1244 render_help(&spec, &spec.cmd, false),
1245 render_help(&spec, &spec.cmd, true),
1246 ] {
1247 let commands = page.find("\nCommands:\n").expect("default command section");
1248 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
1249 let core = page.find("\nCore commands:\n").expect("core section");
1250 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
1251 assert!(commands < core && commands < maintenance, "{page}");
1252 let default_end = core.min(maintenance);
1253 assert!(page[commands..default_end].contains("status"), "{page}");
1254 assert!(page[commands..default_end].contains("help"), "{page}");
1255 assert!(page[core..].contains("run"), "{page}");
1256 assert!(page[maintenance..].contains("clean"), "{page}");
1257 }
1258 }
1259
1260 #[test]
1261 fn test_render_help_with_next_line_layout() {
1262 let spec = crate::spec! { r#"
1263bin "testcli"
1264next_line_help #true
1265arg "<input>" help="Input file" env="INPUT" default="fast" {
1266 choices {
1267 choice "fast"
1268 choice "slow"
1269 }
1270}
1271flag "--verbose" help="Enable verbose output"
1272cmd "run" help="Run it"
1273 "# }
1274 .unwrap();
1275
1276 let short = render_help(&spec, &spec.cmd, false);
1277 assert!(!short.contains(" Run it\n\n help"), "{short}");
1278 for page in [short, render_help(&spec, &spec.cmd, true)] {
1279 assert!(page.contains(" [input]\n Input file"), "{page}");
1280 assert!(
1281 page.contains("--verbose\n Enable verbose output"),
1282 "{page}"
1283 );
1284 assert!(
1285 page.contains(
1286 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
1287 ),
1288 "{page}"
1289 );
1290 assert!(page.contains(" run\n Run it"), "{page}");
1291 }
1292 }
1293
1294 #[test]
1295 fn flatten_help_expands_subcommands_instead_of_listing_them() {
1296 let spec = crate::spec! { r#"
1297bin "testcli"
1298flatten_help #true
1299next_line_help #true
1300cmd "run" help="Run it" {
1301 arg "<task>" help="Task name" env="TASK" default="build" {
1302 choices {
1303 choice "build"
1304 choice "test"
1305 }
1306 }
1307 flag "--dry-run" help="Only show changes"
1308 flatten_help #true
1309 cmd "nested" help="Nested operation" {
1310 flag "--deep" help="Deep option"
1311 }
1312}
1313 "# }
1314 .unwrap();
1315
1316 for page in [
1317 render_help(&spec, &spec.cmd, false),
1318 render_help(&spec, &spec.cmd, true),
1319 ] {
1320 assert!(
1321 page.contains("Usage: testcli\n testcli run"),
1322 "{page}"
1323 );
1324 assert!(!page.contains("\nCommands:\n"), "{page}");
1325 assert!(page.contains("\nrun:\nRun it"), "{page}");
1326 assert!(page.contains("[task]"), "{page}");
1327 assert!(page.contains("--dry-run"), "{page}");
1328 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
1329 assert!(page.contains("--deep"), "{page}");
1330 assert!(
1331 page.contains(
1332 " [possible values: build, test]\n [env: TASK]\n (default: build)"
1333 ),
1334 "{page}"
1335 );
1336 }
1337 }
1338}