1use crate::{Spec, SpecCommand};
2use std::collections::HashMap;
3use std::sync::LazyLock;
4use tera::Tera;
5
6mod style;
7pub use style::Style;
8
9pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String {
10 render_help_styled(spec, cmd, long, Style::PLAIN)
11}
12
13pub fn render_help_styled(spec: &Spec, cmd: &SpecCommand, long: bool, style: Style) -> String {
18 render_page(spec, cmd, long, style, ChoiceRuns::Describe)
19}
20
21pub fn render_runtime_help(
32 spec: &Spec,
33 cmd: &SpecCommand,
34 long: bool,
35 style: Style,
36 env: Option<&HashMap<String, String>>,
37) -> String {
38 let _scope = crate::spec::choices::RunScope::enter();
39 render_page(spec, cmd, long, style, ChoiceRuns::Run(env))
40}
41
42#[derive(Clone, Copy)]
44enum ChoiceRuns<'a> {
45 Describe,
46 Run(Option<&'a HashMap<String, String>>),
47}
48
49fn render_page(
50 spec: &Spec,
51 cmd: &SpecCommand,
52 long: bool,
53 style: Style,
54 runs: ChoiceRuns<'_>,
55) -> String {
56 let docs_spec = crate::docs::models::Spec::from(spec);
58 let mut docs_cmd = crate::docs::models::SpecCommand::from(&without_hidden(cmd, long));
59
60 let mut ctx = tera::Context::new();
61 ctx.insert("spec", &docs_spec);
62 ctx.insert("long", &long);
63 ctx.insert("root", &docs_cmd.full_cmd.is_empty());
67 ctx.insert("show_help_subcommand", &!cmd.disable_help_subcommand);
71 let (mut inherited, ancestors_taken) = inherited_flags(spec, cmd, &docs_cmd.full_cmd, long);
78 if let ChoiceRuns::Run(env) = runs {
80 resolve_command_choices(&mut docs_cmd, env);
81 for flag in &mut inherited {
82 resolve_flag_choices(flag, env);
83 }
84 }
85
86 {
94 let supplied = supplied_flags(spec, cmd, &ancestors_taken, docs_cmd.full_cmd.is_empty());
95 if !supplied.is_empty() {
96 match docs_cmd
97 .flag_groups
98 .iter_mut()
99 .find(|g| g.heading.is_none())
100 {
101 Some(group) => group.items.extend(supplied),
102 None => docs_cmd.flag_groups.insert(
107 0,
108 crate::docs::models::Group {
109 heading: None,
110 help: None,
111 help_rendered: None,
112 items: supplied,
113 },
114 ),
115 }
116 }
117 }
118
119 let terminal = crate::docs::layout::help_width(cmd.term_width, cmd.max_term_width);
123 let logo_margin = docs_cmd
124 .full_cmd
125 .is_empty()
126 .then_some(spec.logo.as_deref())
127 .flatten()
128 .and_then(|logo| crate::docs::logo::margin(logo, terminal));
129 let width = logo_margin.map_or(terminal, |(page, _)| page);
130 ctx.insert("terminal_width", &width);
131 lay_out_group_help(&mut docs_cmd.subcommand_groups, width);
132 lay_out_group_help(&mut docs_cmd.arg_groups, width);
133 lay_out_group_help(&mut docs_cmd.flag_groups, width);
134 let col = crate::docs::layout::usage_column_width(
135 docs_cmd
136 .flag_groups
137 .iter()
138 .flat_map(|g| g.items.iter())
139 .chain(inherited.iter())
140 .map(|f| f.display_usage.as_str()),
141 width,
142 );
143 let flag_column = Column {
144 width,
145 col,
146 long,
147 next_line: cmd.next_line_help,
148 };
149 for group in &mut docs_cmd.flag_groups {
150 lay_out(&mut group.items, flag_column);
151 }
152 lay_out(&mut inherited, flag_column);
153
154 let arg_col = crate::docs::layout::usage_column_width(
158 docs_cmd.args.iter().map(|a| a.usage.as_str()),
159 width,
160 );
161 for group in &mut docs_cmd.arg_groups {
162 lay_out_args(
163 &mut group.items,
164 Column {
165 width,
166 col: arg_col,
167 long,
168 next_line: cmd.next_line_help,
169 },
170 );
171 }
172
173 lay_out_flattened(&mut docs_cmd.flattened_subcommands, width, long);
176
177 let help_row = {
182 let show_help_row = !docs_cmd.subcommands.is_empty()
183 && !docs_cmd.flatten_help
184 && !cmd.disable_help_subcommand;
185 let cmd_col = crate::docs::layout::usage_column_width(
186 docs_cmd
187 .subcommand_groups
188 .iter()
189 .flat_map(|g| g.items.iter())
190 .map(|c| c.name.as_str())
191 .chain(show_help_row.then_some(HELP_SUBCOMMAND)),
192 width,
193 );
194 let default_name = docs_cmd
195 .full_cmd
196 .is_empty()
197 .then(|| {
198 spec.default_subcommand.as_deref().and_then(|name| {
199 spec.cmd
200 .find_subcommand(name)
201 .filter(|child| !child.hide)
202 .map(|child| child.name.as_str())
203 })
204 })
205 .flatten();
206 for group in &mut docs_cmd.subcommand_groups {
207 lay_out_commands(
208 &mut group.items,
209 width,
210 cmd_col,
211 cmd.next_line_help,
212 default_name,
213 );
214 }
215 show_help_row.then(|| {
219 render_row(
220 HELP_SUBCOMMAND,
221 HELP_SUBCOMMAND_SUMMARY,
222 width,
223 cmd_col,
224 cmd.next_line_help,
225 )
226 })
227 };
228 ctx.insert("help_row", &help_row);
229
230 let arg_has_ungrouped = docs_cmd
231 .arg_groups
232 .iter()
233 .any(|group| group.heading.is_none());
234 let arg_has_grouped = docs_cmd
235 .arg_groups
236 .iter()
237 .any(|group| group.heading.is_some());
238 let flag_has_ungrouped = docs_cmd
239 .flag_groups
240 .iter()
241 .any(|group| group.heading.is_none());
242 let flag_has_grouped = docs_cmd
243 .flag_groups
244 .iter()
245 .any(|group| group.heading.is_some());
246 ctx.insert("arg_has_ungrouped", &arg_has_ungrouped);
247 ctx.insert("arg_has_grouped", &arg_has_grouped);
248 ctx.insert("flag_has_ungrouped", &flag_has_ungrouped);
249 ctx.insert("flag_has_grouped", &flag_has_grouped);
250
251 ctx.insert("cmd", &docs_cmd);
254 ctx.insert("global_flags", &inherited);
255 for (name, mark) in MARKS {
256 ctx.insert(name, &mark);
257 }
258 ctx.insert("mark_grouped_args", &MARK_GROUPED_ARGS);
259 ctx.insert("mark_grouped_flags", &MARK_GROUPED_FLAGS);
260 ctx.insert("mark_global_flags", &MARK_GLOBAL_FLAGS);
261 let template = if long {
262 "spec_template_long.tera"
263 } else {
264 "spec_template_short.tera"
265 };
266 let rendered = TERA.render(template, &ctx).unwrap();
267 let sections = Sections::split(&rendered);
268 let styling = style::Styling::new(
269 &docs_cmd,
270 &inherited,
271 sections.usage,
272 !cmd.disable_help_subcommand,
273 );
274 let page = match spec
275 .help_template
276 .as_deref()
277 .filter(|t| crate::help_template::is_set(t))
278 {
279 Some(template) => {
280 crate::help_template::substitute_with_style(template, style.coloured, |name| {
281 sections
282 .named(name)
283 .map(|section| styling.apply(§ion, style))
284 })
285 }
286 None => styling.apply(§ions.concatenated(), style),
287 };
288 let page = if style.coloured {
289 std::borrow::Cow::Borrowed(page.as_str())
290 } else {
291 crate::docs::strip_ansi(&page)
292 };
293 let mut page = page.trim().to_string();
294 page.push('\n');
295 if docs_cmd.full_cmd.is_empty()
299 && let Some(logo) = spec.logo.as_deref()
300 {
301 page = crate::docs::logo::place(
302 &page,
303 logo,
304 spec.logo_style.as_deref(),
305 terminal,
306 logo_margin.map(|(_, column)| column),
307 style.coloured,
308 );
309 }
310 append_default_command_help(spec, cmd, long, style, runs, page)
311}
312
313fn resolve_command_choices(
315 cmd: &mut crate::docs::models::SpecCommand,
316 env: Option<&HashMap<String, String>>,
317) {
318 for arg in cmd
319 .args
320 .iter_mut()
321 .chain(cmd.arg_groups.iter_mut().flat_map(|g| g.items.iter_mut()))
322 {
323 resolve_arg_choices(arg, env);
324 }
325 for flag in cmd
326 .flags
327 .iter_mut()
328 .chain(cmd.flag_groups.iter_mut().flat_map(|g| g.items.iter_mut()))
329 {
330 resolve_flag_choices(flag, env);
331 }
332 for child in &mut cmd.flattened_subcommands {
333 resolve_command_choices(child, env);
334 }
335}
336
337fn resolve_flag_choices(
338 flag: &mut crate::docs::models::SpecFlag,
339 env: Option<&HashMap<String, String>>,
340) {
341 if flag.hide_possible_values {
344 return;
345 }
346 if let Some(arg) = &mut flag.arg {
347 resolve_arg_choices(arg, env);
348 }
349}
350
351fn resolve_arg_choices(
352 arg: &mut crate::docs::models::SpecArg,
353 env: Option<&HashMap<String, String>>,
354) {
355 if arg.hide_possible_values {
356 return;
357 }
358 if let Some(choices) = arg.choices.as_mut().filter(|c| c.run.is_some()) {
359 choices.resolve_for_help(env);
360 }
361}
362
363fn append_default_command_help(
364 spec: &Spec,
365 cmd: &SpecCommand,
366 long: bool,
367 style: Style,
368 runs: ChoiceRuns<'_>,
369 parent: String,
370) -> String {
371 if !cmd.full_cmd.is_empty() || !spec.default_subcommand_help || cmd.flatten_help {
372 return parent;
373 }
374 let Some(child) = spec
375 .default_subcommand
376 .as_deref()
377 .and_then(|name| spec.cmd.find_subcommand(name))
378 .filter(|child| !child.hide)
379 else {
380 return parent;
381 };
382 let child_page = render_page(spec, child, long, style, runs);
383 let mut out = parent.trim_end().to_string();
384 out.push_str("\n\nDefault command: ");
385 out.push_str(&crate::help_template::semantic(
386 "command",
387 &child.name,
388 style.coloured,
389 ));
390 out.push_str("\n Unmatched words select this command.\n\n");
391 out.push_str(child_page.trim_end());
392 out.push('\n');
393 out
394}
395
396const MARKS: [(&str, &str); 6] = [
407 ("mark_usage", "\u{1}usage\u{1}"),
408 ("mark_commands", "\u{1}commands\u{1}"),
409 ("mark_args", "\u{1}args\u{1}"),
410 ("mark_flags", "\u{1}flags\u{1}"),
411 ("mark_flattened", "\u{1}flattened\u{1}"),
412 ("mark_after_help", "\u{1}after_help\u{1}"),
413];
414const MARK_GROUPED_ARGS: &str = "\u{1}grouped_args\u{1}";
415const MARK_GROUPED_FLAGS: &str = "\u{1}grouped_flags\u{1}";
416const MARK_GLOBAL_FLAGS: &str = "\u{1}global_flags\u{1}";
417
418struct Sections<'a> {
424 about: &'a str,
425 usage: &'a str,
426 commands: &'a str,
427 args: String,
428 flags: String,
429 grouped_args: &'a str,
430 ungrouped_args: &'a str,
431 grouped_flags: &'a str,
432 ungrouped_flags: String,
433 flattened: &'a str,
434 after_help: &'a str,
435}
436
437impl<'a> Sections<'a> {
438 fn split(rendered: &'a str) -> Self {
439 let mut rest = rendered;
440 let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
441 for (_, mark) in MARKS {
442 match rest.split_once(mark) {
446 Some((before, after)) => {
447 parts.push(before);
448 rest = after;
449 }
450 None => parts.push(""),
451 }
452 }
453 parts.push(rest);
454 let (ungrouped_args, grouped_args) = parts[3]
455 .split_once(MARK_GROUPED_ARGS)
456 .unwrap_or((parts[3], ""));
457 let (own_flags, global_flags) = parts[4]
458 .split_once(MARK_GLOBAL_FLAGS)
459 .unwrap_or((parts[4], ""));
460 let (own_ungrouped_flags, grouped_flags) = own_flags
461 .split_once(MARK_GROUPED_FLAGS)
462 .unwrap_or((own_flags, ""));
463 Self {
464 about: parts[0],
465 usage: parts[1],
466 commands: parts[2],
467 args: format!("{ungrouped_args}{grouped_args}"),
468 flags: format!("{own_ungrouped_flags}{grouped_flags}{global_flags}"),
469 grouped_args,
470 ungrouped_args,
471 grouped_flags,
472 ungrouped_flags: format!("{own_ungrouped_flags}{global_flags}"),
473 flattened: parts[5],
474 after_help: parts[6],
475 }
476 }
477
478 fn concatenated(&self) -> String {
480 [
481 self.about,
482 self.usage,
483 self.commands,
484 self.args.as_str(),
485 self.flags.as_str(),
486 self.flattened,
487 self.after_help,
488 ]
489 .concat()
490 }
491
492 fn named(&self, name: &str) -> Option<String> {
494 Some(match name {
495 "about" => self.about.trim().to_string(),
496 "usage" => self.usage.trim().to_string(),
497 "commands" => {
498 let mut out = self.commands.trim().to_string();
499 let flattened = self.flattened.trim();
500 if !flattened.is_empty() {
501 if !out.is_empty() {
502 out.push_str("\n\n");
503 }
504 out.push_str(flattened);
505 }
506 out
507 }
508 "args" => self.args.trim().to_string(),
509 "flags" => self.flags.trim().to_string(),
510 "grouped_args" => self.grouped_args.trim().to_string(),
511 "ungrouped_args" => self.ungrouped_args.trim().to_string(),
512 "grouped_flags" => self.grouped_flags.trim().to_string(),
513 "ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
514 "after_help" => self.after_help.trim().to_string(),
515 _ => return None,
516 })
517 }
518}
519
520fn supplied_flags(
534 spec: &Spec,
535 cmd: &SpecCommand,
536 ancestors_taken: &[String],
537 is_root: bool,
538) -> Vec<crate::docs::models::SpecFlag> {
539 let mut taken: Vec<String> = ancestors_taken.to_vec();
544 for f in &cmd.flags {
545 taken.extend(f.long.iter().map(|l| format!("--{l}")));
546 taken.extend(f.short.iter().map(|s| format!("-{s}")));
547 taken.extend(f.negate.clone());
549 }
550
551 let build = |name: &str, long: &str, short: char, help: &str| {
552 let long_free = !taken.contains(&format!("--{long}"));
553 let short_free = !taken.contains(&format!("-{short}"));
554 if !long_free && !short_free {
555 return None;
556 }
557 let name = if long_free { name } else { &short.to_string() };
560 let mut flag = crate::SpecFlag {
561 name: name.to_string(),
562 long: if long_free {
563 vec![long.to_string()]
564 } else {
565 vec![]
566 },
567 short: if short_free { vec![short] } else { vec![] },
568 help: Some(help.to_string()),
569 ..Default::default()
570 };
571 flag.usage = flag.usage();
572 Some(crate::docs::models::SpecFlag::from(&flag))
573 };
574
575 let mut out = Vec::new();
576 if spec.disable_help != Some(true) && !cmd.disable_help_flag {
583 out.extend(build("help", "help", 'h', "Print help"));
584 }
585 if is_root
586 && (spec.version.is_some() || spec.long_version.is_some())
587 && !cmd.disable_version_flag
588 {
589 out.extend(build("version", "version", 'V', "Print version"));
590 }
591 out
592}
593
594fn lay_out(flags: &mut [crate::docs::models::SpecFlag], column: Column) {
601 for flag in flags {
602 flag.usage_col_width = column.col;
603 flag.help_is_block =
604 !column.can_inline(crate::docs::layout::visible_width(&flag.display_usage));
605 let text = if column.long {
606 flag.help_long
607 .as_deref()
608 .or(flag.help.as_deref())
609 .map(str::to_string)
610 } else if column.next_line {
611 flag.help.clone()
612 } else {
613 with_annotations(flag.help.as_deref(), flag_annotations(flag))
614 };
615 wrap_into(
616 text,
617 column,
618 crate::docs::layout::visible_width(&flag.display_usage),
619 &mut flag.row,
620 &mut flag.help_rendered,
621 &mut flag.help_is_multiline,
622 &mut flag.ann_indent,
623 );
624 }
625}
626
627fn lay_out_args(args: &mut [crate::docs::models::SpecArg], column: Column) {
633 for arg in args {
634 arg.usage_col_width = column.col;
635 arg.help_is_block = !column.can_inline(crate::docs::layout::visible_width(&arg.usage));
636 let text = if column.long {
637 arg.help_long
638 .as_deref()
639 .or(arg.help.as_deref())
640 .map(str::to_string)
641 } else if column.next_line {
642 arg.help.clone()
643 } else {
644 with_annotations(arg.help.as_deref(), arg_annotations(arg))
645 };
646 wrap_into(
647 text,
648 column,
649 crate::docs::layout::visible_width(&arg.usage),
650 &mut arg.row,
651 &mut arg.help_rendered,
652 &mut arg.help_is_multiline,
653 &mut arg.ann_indent,
654 );
655 }
656}
657
658fn lay_out_flattened(commands: &mut [crate::docs::models::SpecCommand], width: usize, long: bool) {
659 for command in commands {
660 lay_out_group_help(&mut command.arg_groups, width);
661 lay_out_group_help(&mut command.flag_groups, width);
662 let arg_col = crate::docs::layout::usage_column_width(
663 command
664 .arg_groups
665 .iter()
666 .flat_map(|group| group.items.iter())
667 .map(|arg| arg.usage.as_str()),
668 width,
669 );
670 let arg_column = Column {
671 width,
672 col: arg_col,
673 long,
674 next_line: command.flattened_next_line_help,
675 };
676 for group in &mut command.arg_groups {
677 lay_out_args(&mut group.items, arg_column);
678 }
679
680 let flag_col = crate::docs::layout::usage_column_width(
681 command
682 .flag_groups
683 .iter()
684 .flat_map(|group| group.items.iter())
685 .map(|flag| flag.display_usage.as_str()),
686 width,
687 );
688 let flag_column = Column {
689 width,
690 col: flag_col,
691 long,
692 next_line: command.flattened_next_line_help,
693 };
694 for group in &mut command.flag_groups {
695 lay_out(&mut group.items, flag_column);
696 }
697 }
698}
699
700fn lay_out_group_help<T>(groups: &mut [crate::docs::models::Group<T>], width: usize) {
701 for group in groups {
702 group.help_rendered = group
703 .help
704 .as_deref()
705 .map(|help| crate::docs::layout::render_indented_text(help, width, 2));
706 }
707}
708
709fn wrap_into(
715 text: Option<String>,
716 column: Column,
717 usage_width: usize,
718 row: &mut Option<String>,
719 help_rendered: &mut Option<String>,
720 help_is_multiline: &mut bool,
721 ann_indent: &mut String,
722) {
723 *row = None;
724 *help_rendered = None;
725 *help_is_multiline = false;
726 *ann_indent = column.annotation_indent(column.can_inline(usage_width));
729 let Some(text) = text else { return };
730 if !column.can_inline(usage_width) {
731 let indent = column.block_indent();
732 *row = Some(indent_text(
733 &crate::docs::layout::render_indented_text(&text, column.width, indent),
734 indent,
735 ));
736 return;
737 }
738 let first_indent = column.inline_help_start(usage_width);
739 let continuation_indent = column.description_indent();
740 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
741 &text,
742 column.width,
743 first_indent,
744 continuation_indent,
745 );
746 *ann_indent = column.annotation_indent(!rendered.is_empty());
750 if !rendered.is_empty() {
751 *help_rendered = Some(rendered);
752 *help_is_multiline = is_multiline;
753 }
754 *row = Some(text);
755}
756
757fn indent_text(text: &str, indent: usize) -> String {
758 let pad = " ".repeat(indent);
759 text.lines()
760 .map(|line| {
761 if line.is_empty() {
762 String::new()
763 } else {
764 format!("{pad}{line}")
765 }
766 })
767 .collect::<Vec<_>>()
768 .join("\n")
769}
770
771fn with_annotations(help: Option<&str>, annotations: Vec<String>) -> Option<String> {
778 let mut parts = Vec::new();
779 if let Some(help) = summarize(help) {
780 parts.push(help.to_string());
781 }
782 parts.extend(annotations);
783 (!parts.is_empty()).then(|| parts.join(" "))
784}
785
786fn flag_annotations(flag: &crate::docs::models::SpecFlag) -> Vec<String> {
788 let mut parts = value_annotations(
789 flag.arg.as_ref().and_then(|arg| arg.choices.as_ref()),
790 flag.hide_possible_values,
791 flag.env.as_deref(),
792 flag.hide_env,
793 &flag.env_fallback,
794 &flag.deprecated_env,
795 &flag.default,
796 flag.hide_default_value,
797 );
798 if let Some(label) = deprecation_label(
799 flag.deprecated.as_deref(),
800 flag.deprecated_warn_at.as_deref(),
801 flag.deprecated_remove_at.as_deref(),
802 ) {
803 parts.push(label);
804 }
805 parts
806}
807
808fn arg_annotations(arg: &crate::docs::models::SpecArg) -> Vec<String> {
810 value_annotations(
811 arg.choices.as_ref(),
812 arg.hide_possible_values,
813 arg.env.as_deref(),
814 arg.hide_env,
815 &arg.env_fallback,
816 &arg.deprecated_env,
817 &arg.default,
818 arg.hide_default_value,
819 )
820}
821
822#[allow(clippy::too_many_arguments)]
824fn value_annotations(
825 choices: Option<&crate::SpecChoices>,
826 hide_possible_values: bool,
827 env: Option<&str>,
828 hide_env: bool,
829 env_fallback: &[String],
830 deprecated_env: &[String],
831 default: &[String],
832 hide_default_value: bool,
833) -> Vec<String> {
834 let mut parts = Vec::new();
835 if let Some(choices) = choices.filter(|_| !hide_possible_values) {
836 if !choices.choices.is_empty() {
837 parts.push(format!("[{}]", choices.choices.join(", ")));
838 }
839 if let Some(env) = choices.env() {
840 parts.push(format!("[choices env: {env}]"));
841 }
842 if let Some(run) = choices.run() {
843 parts.push(format!("[output of `{run}`]"));
845 }
846 }
847 if !hide_env {
848 if let Some(env) = env {
849 parts.push(format!("[env: {env}]"));
850 }
851 parts.extend(
852 env_fallback
853 .iter()
854 .map(|env| format!("[env fallback: {env}]")),
855 );
856 parts.extend(
857 deprecated_env
858 .iter()
859 .map(|env| format!("[deprecated env: {env}]")),
860 );
861 }
862 if !hide_default_value && !default.is_empty() {
863 parts.push(format!("(default: {})", default.join(", ")));
864 }
865 parts
866}
867
868#[derive(Clone, Copy)]
870struct Column {
871 width: usize,
873 col: usize,
875 long: bool,
877 next_line: bool,
879}
880
881const BLOCK_INDENT: usize = 4;
883
884impl Column {
885 fn usage_overflows(&self, usage_width: usize) -> bool {
886 !self.next_line && usage_width > self.col
887 }
888
889 fn description_indent(&self) -> usize {
890 2 + self.col + 2
891 }
892
893 fn inline_help_start(&self, usage_width: usize) -> usize {
894 if self.usage_overflows(usage_width) {
895 2 + usage_width + 2
896 } else {
897 self.description_indent()
898 }
899 }
900
901 fn can_inline(&self, usage_width: usize) -> bool {
903 if self.next_line {
904 return false;
905 }
906 let minimum = if self.usage_overflows(usage_width) {
907 crate::docs::layout::MIN_INLINE_HELP_WIDTH
908 } else {
909 10
910 };
911 self.width
912 .saturating_sub(self.inline_help_start(usage_width))
913 >= minimum
914 }
915
916 fn annotation_indent(&self, reached_column: bool) -> String {
923 let indent = if reached_column {
924 self.description_indent()
925 } else {
926 self.block_indent()
927 };
928 " ".repeat(indent)
929 }
930
931 fn block_indent(&self) -> usize {
934 let description = self.description_indent();
935 if !self.next_line && self.width.saturating_sub(description) >= 10 {
936 description
937 } else {
938 BLOCK_INDENT
939 }
940 }
941}
942
943const HELP_SUBCOMMAND: &str = "help";
945const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
946
947fn lay_out_commands(
952 commands: &mut [crate::docs::models::HelpCommand],
953 terminal_width: usize,
954 col: usize,
955 next_line: bool,
956 default_name: Option<&str>,
957) {
958 let column = Column {
959 width: terminal_width,
960 col,
961 long: false,
962 next_line,
963 };
964 for command in commands {
965 command.usage_col_width = col;
966 command.row = command_row(command, default_name == Some(command.name.as_str()));
967 command.help_rendered = None;
968 command.help_is_multiline = false;
969 let usage_width = crate::docs::layout::visible_width(&command.name);
970 if let Some(row) = command.row.as_deref() {
971 if column.can_inline(usage_width) {
972 let (rendered, is_multiline) = crate::docs::layout::render_help_text_at(
973 row,
974 terminal_width,
975 column.inline_help_start(usage_width),
976 column.description_indent(),
977 );
978 if !rendered.is_empty() {
979 command.help_rendered = Some(rendered);
980 command.help_is_multiline = is_multiline;
981 }
982 } else if let Some(row) = command.row.as_mut() {
983 let indent = column.block_indent();
984 *row = indent_text(
985 &crate::docs::layout::render_indented_text(row, terminal_width, indent),
986 indent,
987 );
988 }
989 }
990 }
991}
992
993fn command_row(cmd: &crate::docs::models::HelpCommand, is_default: bool) -> Option<String> {
1000 let mut parts = Vec::new();
1001 let summary = summarize(cmd.help.as_deref()).or_else(|| {
1004 summarize(
1005 cmd.help_long
1006 .as_deref()
1007 .and_then(|help| help.lines().next()),
1008 )
1009 });
1010 if let Some(summary) = summary {
1011 parts.push(summary.to_string());
1012 }
1013 if !cmd.aliases.is_empty() {
1014 parts.push(format!("[aliases: {}]", cmd.aliases.join(", ")));
1015 }
1016 if let Some(label) = deprecation_label(
1017 cmd.deprecated.as_deref(),
1018 cmd.deprecated_warn_at.as_deref(),
1019 cmd.deprecated_remove_at.as_deref(),
1020 ) {
1021 parts.push(label);
1022 }
1023 if is_default {
1024 parts.push("(default)".to_string());
1025 }
1026 (!parts.is_empty()).then(|| parts.join(" "))
1027}
1028
1029fn summarize(text: Option<&str>) -> Option<&str> {
1031 text.map(str::trim_end).filter(|text| !text.is_empty())
1032}
1033
1034fn deprecation_label(
1036 message: Option<&str>,
1037 warn_at: Option<&str>,
1038 remove_at: Option<&str>,
1039) -> Option<String> {
1040 if message.is_none() && warn_at.is_none() && remove_at.is_none() {
1041 return None;
1042 }
1043 let mut parts = Vec::new();
1044 if let Some(message) = message {
1045 parts.push(message.to_string());
1046 }
1047 if let Some(at) = warn_at {
1048 parts.push(format!("warns at {at}"));
1049 }
1050 if let Some(at) = remove_at {
1051 parts.push(format!("removed at {at}"));
1052 }
1053 Some(format!("[deprecated: {}]", parts.join("; ")))
1054}
1055
1056fn render_row(
1058 name: &str,
1059 row: &str,
1060 terminal_width: usize,
1061 col: usize,
1062 next_line_help: bool,
1063) -> String {
1064 if !next_line_help {
1065 if crate::docs::layout::visible_width(name) <= col {
1066 let (rendered, _) = crate::docs::layout::render_help_text(row, terminal_width, col);
1067 if !rendered.is_empty() {
1068 return format!(" {name:<col$} {rendered}");
1069 }
1070 } else if !row.contains('\n') {
1071 return format!(
1072 " {name}\n {}",
1073 crate::docs::layout::render_block_text(row, terminal_width)
1074 );
1075 }
1076 }
1077 format!(" {name}\n {row}")
1078}
1079
1080fn inherited_flags(
1089 spec: &Spec,
1090 cmd: &SpecCommand,
1091 full_cmd: &[String],
1092 long_help: bool,
1093) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
1094 let mut ancestors: Vec<&SpecCommand> = Vec::new();
1096 let mut at = &spec.cmd;
1097 for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
1098 ancestors.push(at);
1099 let Some(next) = at.subcommands.get(name) else {
1100 return (Vec::new(), Vec::new());
1101 };
1102 at = next;
1103 }
1104 if !full_cmd.is_empty() {
1105 ancestors.push(at);
1106 }
1107
1108 let forms = |f: &crate::SpecFlag| -> Vec<String> {
1121 f.long
1122 .iter()
1123 .map(|l| format!("--{l}"))
1124 .chain(f.short.iter().map(|s| format!("-{s}")))
1125 .collect()
1126 };
1127 let every_form: Vec<String> = cmd
1128 .flags
1129 .iter()
1130 .chain(
1131 ancestors
1132 .iter()
1133 .flat_map(|a| a.flags.iter())
1134 .filter(|f| f.global),
1135 )
1136 .flat_map(&forms)
1137 .collect();
1138
1139 let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
1140 let mut taken_negations: Vec<String> =
1141 cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
1142 let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
1143 for ancestor in ancestors.iter().rev() {
1144 for f in ancestor.flags.iter().filter(|f| f.global) {
1145 let long = f
1146 .long
1147 .iter()
1148 .find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
1149 .cloned();
1150 let short = f
1151 .short
1152 .iter()
1153 .find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
1154 .copied();
1155 let mine = forms(f);
1156 let negate = f.negate.as_ref().is_some_and(|n| {
1157 !taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
1158 });
1159 taken.extend(forms(f));
1162 taken_negations.extend(f.negate.clone());
1163 if f.hide
1164 || if long_help {
1165 f.hide_long_help
1166 } else {
1167 f.hide_short_help
1168 }
1169 || (long.is_none() && short.is_none() && !negate)
1170 {
1171 continue;
1172 }
1173 keep.push((f, long, short, negate));
1174 }
1175 }
1176 let shown: Vec<crate::docs::models::SpecFlag> = ancestors
1177 .iter()
1178 .flat_map(|a| a.flags.iter())
1179 .filter_map(|f| {
1180 keep.iter()
1181 .find(|(k, _, _, _)| std::ptr::eq(*k, f))
1182 .map(|(_, l, s, n)| (f, l.clone(), *s, *n))
1183 })
1184 .map(|(f, long, short, negate)| {
1185 let mut shown = f.clone();
1188 shown.long = long.into_iter().collect();
1189 shown.short = short.into_iter().collect();
1190 if !negate {
1191 shown.negate = None;
1192 }
1193 shown.usage = shown.usage();
1194 crate::docs::models::SpecFlag::from(&shown)
1195 })
1196 .collect();
1197 taken.extend(taken_negations);
1201 (shown, taken)
1202}
1203
1204fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
1216 let mut visible = cmd.clone();
1217 visible.flags.retain(|flag| {
1218 !flag.hide
1219 && if long {
1220 !flag.hide_long_help
1221 } else {
1222 !flag.hide_short_help
1223 }
1224 });
1225 visible.args.retain(|arg| {
1226 !arg.hide
1227 && if long {
1228 !arg.hide_long_help
1229 } else {
1230 !arg.hide_short_help
1231 }
1232 });
1233 visible.subcommands.retain(|_, sub| !sub.hide);
1234 if visible.flatten_help {
1240 for sub in visible.subcommands.values_mut() {
1241 *sub = without_hidden(sub, long);
1242 }
1243 }
1244 visible
1245}
1246
1247static TERA: LazyLock<Tera> = LazyLock::new(|| {
1248 let mut tera = Tera::default();
1249
1250 tera.register_filter(
1252 "ljust",
1253 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1254 let value = value.as_str().unwrap_or("");
1255 let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
1256 Ok(format!("{:<width$}", value, width = width))
1257 },
1258 );
1259 tera.register_filter(
1260 "default",
1261 |value: &tera::Value,
1262 kwargs: tera::Kwargs,
1263 _: &tera::State|
1264 -> tera::TeraResult<tera::Value> {
1265 let default_val = kwargs.must_get::<tera::Value>("value")?;
1266 let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
1267 if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
1268 Ok(default_val)
1269 } else {
1270 Ok(value.clone())
1271 }
1272 },
1273 );
1274 tera.register_filter(
1275 "terminal_wrap",
1276 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1277 let value = value.as_str().unwrap_or("");
1278 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1279 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1280 Ok(crate::docs::layout::render_indented_text(
1281 value, width, indent,
1282 ))
1283 },
1284 );
1285 tera.register_filter(
1286 "terminal_label",
1287 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1288 let value = value.as_str().unwrap_or("");
1289 let label = args.must_get::<String>("label")?;
1290 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1291 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1292 Ok(crate::docs::layout::render_labelled_text(
1293 &label, value, width, indent,
1294 ))
1295 },
1296 );
1297 tera.register_filter(
1298 "terminal_annotation",
1299 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1300 let body = if let Some(values) = value.as_array() {
1301 values
1302 .iter()
1303 .filter_map(tera::Value::as_str)
1304 .collect::<Vec<_>>()
1305 .join(", ")
1306 } else {
1307 value.as_str().unwrap_or("").to_string()
1308 };
1309 let label = args.get::<String>("label")?.unwrap_or_default();
1310 let suffix = args.get::<String>("suffix")?.unwrap_or_default();
1311 let indent = args
1312 .get::<String>("indent")?
1313 .unwrap_or_default()
1314 .chars()
1315 .count();
1316 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1317 let rendered = crate::docs::layout::render_indented_text(
1318 &format!("{label}{body}{suffix}"),
1319 width,
1320 indent,
1321 );
1322 Ok(indent_text(&rendered, indent))
1323 },
1324 );
1325 tera.register_filter(
1326 "terminal_deprecation",
1327 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1328 let field = |name| value.get_from_path(name).and_then(tera::Value::as_str);
1329 let Some(label) = deprecation_label(
1330 field("deprecated"),
1331 field("deprecated_warn_at"),
1332 field("deprecated_remove_at"),
1333 ) else {
1334 return Ok(String::new());
1335 };
1336 let indent = args
1337 .get::<String>("indent")?
1338 .unwrap_or_default()
1339 .chars()
1340 .count();
1341 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1342 let rendered = crate::docs::layout::render_indented_text(&label, width, indent);
1343 Ok(indent_text(&rendered, indent))
1344 },
1345 );
1346
1347 #[rustfmt::skip]
1348 tera.add_raw_templates([
1349 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
1350 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
1351 ]).unwrap();
1352
1353 tera
1354});
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359 use insta::assert_snapshot;
1360
1361 #[test]
1362 fn flag_aliases_do_not_leak_into_interactive_help() {
1363 let spec = crate::spec! { r#"
1364bin "ex"
1365flag "-t -f --tail --follow" help="Follow output"
1366 "# }
1367 .unwrap();
1368
1369 for long in [false, true] {
1370 let page = super::render_help(&spec, &spec.cmd, long);
1371 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1372 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1373 }
1374 }
1375
1376 #[test]
1377 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1378 let spec = crate::spec! { r#"
1383bin "ex"
1384flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1385cmd inner help="a command" {
1386 flag "--plain" help="its own"
1387}
1388 "# }
1389 .unwrap();
1390
1391 let inner = spec.cmd.subcommands.get("inner").expect("inner");
1392 for long in [false, true] {
1393 let page = super::render_help(&spec, inner, long);
1394 assert!(
1395 !page.contains("--help"),
1396 "long={long}: a hidden ancestor binds this:\n{page}"
1397 );
1398 assert!(page.contains("-h"), "long={long}:\n{page}");
1400 }
1401 }
1402
1403 #[test]
1404 fn a_long_beats_a_negation_however_far_away_it_is() {
1405 let spec = crate::spec! { r#"
1411bin "ex"
1412flag "--no-cache" global=#true help="the root's plain long"
1413flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1414cmd narrow help="a command" {
1415 flag "--cache" negate="--no-cache" help="its own, with a negation"
1416 flag "--tint" negate="--no-colour" help="claims the root's negation"
1417}
1418 "# }
1419 .unwrap();
1420
1421 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1422 for long in [false, true] {
1423 let page = super::render_help(&spec, narrow, long);
1424 assert!(
1425 page.contains("--no-cache"),
1426 "long={long}: a long beats a negation, so this still binds here:\n{page}"
1427 );
1428 assert!(page.contains("--colour"), "long={long}:\n{page}");
1431 let global = page
1432 .split_once("Global flags:")
1433 .expect("a global section")
1434 .1;
1435 assert!(
1436 !global.contains("--colour / --no-colour"),
1437 "long={long}: the nearer negation owns that spelling:\n{page}"
1438 );
1439 }
1440 }
1441
1442 #[test]
1443 fn a_description_of_only_spaces_is_no_description() {
1444 let spec = crate::spec! { r#"
1451bin "ex"
1452flag "--blank" help=" "
1453flag "--plain" help="plain"
1454 "# }
1455 .unwrap();
1456
1457 for long in [false, true] {
1458 let page = super::render_help(&spec, &spec.cmd, long);
1459 let listing = page.split_once("\nFlags:").expect("a flags section").1;
1462 let line = listing
1463 .lines()
1464 .find(|l| l.contains("--blank"))
1465 .unwrap_or_else(|| panic!("long={long}: {page}"));
1466 assert_eq!(
1467 line,
1468 line.trim_end(),
1469 "long={long}: trailing space on {line:?}"
1470 );
1471 }
1472 }
1473
1474 #[test]
1475 fn test_render_help_omits_hidden_entries() {
1476 let spec = crate::spec! { r#"
1477bin "ex"
1478flag "--visible" help="shown"
1479flag "--secret" hide=#true help="hidden"
1480flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1481arg "[SHOWN]" help="an arg"
1482arg "[HIDDEN]" hide=#true help="a hidden arg"
1483cmd open help="a command"
1484cmd sneaky hide=#true help="a hidden command"
1485 "# }
1486 .unwrap();
1487
1488 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1493 Usage: ex [--visible] [SHOWN] [SUBCOMMAND]
1494
1495 Commands:
1496 open a command
1497 help Print this message or the help of the given subcommand(s)
1498
1499 Arguments:
1500 [SHOWN] an arg
1501
1502 Flags:
1503 --visible shown
1504 -h, --help Print help
1505 ");
1506 }
1507
1508 #[test]
1509 fn test_render_help_groups_by_heading() {
1510 let spec = crate::spec! { r#"
1511bin "testcli"
1512flag "--verbose" help="Verbose output"
1513flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1514flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1515flag "--jobs <n>" help="How many at once" help_heading="Performance"
1516arg "<file>" help="The file"
1517arg "<mode>" help="How to run" help_heading="Behaviour"
1518 "# }
1519 .unwrap();
1520
1521 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1524 Usage: testcli [FLAGS] <file> <mode>
1525
1526 Arguments:
1527 <file> The file
1528
1529 Behaviour:
1530 <mode> How to run
1531
1532 Flags:
1533 --verbose Verbose output
1534 -h, --help Print help
1535
1536 Filtering:
1537 --filter <pattern> Only matching
1538 --exclude <pattern> Skip matching
1539
1540 Performance:
1541 --jobs <n> How many at once
1542 ");
1543 }
1544
1545 #[test]
1546 fn test_render_help_with_only_headed_flags() {
1547 let spec = crate::spec! { r#"
1550bin "testcli"
1551flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1552 "# }
1553 .unwrap();
1554
1555 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1556 Usage: testcli [--filter <pattern>]
1557
1558 Flags:
1559 -h, --help Print help
1560
1561 Filtering:
1562 --filter <pattern> Only matching
1563 ");
1564 }
1565
1566 #[test]
1567 fn test_render_help_with_env() {
1568 let spec = crate::spec! { r#"
1569bin "testcli"
1570flag "--color" env="MYCLI_COLOR" help="Enable color output"
1571flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1572flag "--debug" help="Debug mode"
1573 "# }
1574 .unwrap();
1575
1576 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1577 Usage: testcli [FLAGS]
1578
1579 Flags:
1580 --color Enable color output [env: MYCLI_COLOR]
1581 --verbose Verbose output [env: MYCLI_VERBOSE]
1582 --debug Debug mode
1583 -h, --help Print help
1584 ");
1585
1586 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1587 Usage: testcli [FLAGS]
1588
1589 Flags:
1590 --color Enable color output
1591 [env: MYCLI_COLOR]
1592 --verbose Verbose output
1593 [env: MYCLI_VERBOSE]
1594 --debug Debug mode
1595 -h, --help Print help
1596 ");
1597 }
1598
1599 #[test]
1600 fn test_render_help_with_arg_env() {
1601 let spec = crate::spec! { r#"
1602bin "testcli"
1603arg "<input>" env="MY_INPUT" help="Input file"
1604arg "<output>" env="MY_OUTPUT" help="Output file"
1605arg "<extra>" help="Extra arg without env"
1606arg "[default]" help="Arg with default value" default="default value"
1607 "# }
1608 .unwrap();
1609
1610 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1611 Usage: testcli <ARGS>…
1612
1613 Arguments:
1614 <input> Input file [env: MY_INPUT]
1615 <output> Output file [env: MY_OUTPUT]
1616 <extra> Extra arg without env
1617 [default] Arg with default value (default: default value)
1618
1619 Flags:
1620 -h, --help Print help
1621 ");
1622
1623 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1624 Usage: testcli <ARGS>…
1625
1626 Arguments:
1627 <input> Input file
1628 [env: MY_INPUT]
1629 <output> Output file
1630 [env: MY_OUTPUT]
1631 <extra> Extra arg without env
1632 [default] Arg with default value
1633 (default: default value)
1634
1635 Flags:
1636 -h, --help Print help
1637 ");
1638 }
1639
1640 #[test]
1641 fn test_render_help_with_negated_flag() {
1642 let spec = crate::spec! { r#"
1643bin "testcli"
1644flag "--compress" negate="--no-compress" default=#true help="Compress output"
1645flag "--verbose" help="Verbose output"
1646 "# }
1647 .unwrap();
1648
1649 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1650 Usage: testcli [--compress] [--verbose]
1651
1652 Flags:
1653 --compress / --no-compress Compress output (default: true)
1654 --verbose Verbose output
1655 -h, --help Print help
1656 ");
1657
1658 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1659 Usage: testcli [--compress] [--verbose]
1660
1661 Flags:
1662 --compress / --no-compress Compress output
1663 (default: true)
1664 --verbose Verbose output
1665 -h, --help Print help
1666 ");
1667 }
1668
1669 #[test]
1670 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1671 let spec = crate::spec! { r#"
1672bin "testcli"
1673flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1674 choices {
1675 choice "fast"
1676 choice "slow"
1677 }
1678}
1679flag "--short-only" help="short" hide_long_help=#true
1680flag "--long-only" help="long" hide_short_help=#true
1681arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1682 "# }
1683 .unwrap();
1684
1685 let short = render_help(&spec, &spec.cmd, false);
1686 assert!(short.contains("--mode <mode>"), "{short}");
1687 assert!(short.contains("--short-only"), "{short}");
1688 assert!(!short.contains("--long-only"), "{short}");
1689 assert!(
1690 !short.contains("MODE") && !short.contains("fast, slow"),
1691 "{short}"
1692 );
1693 assert!(
1694 !short.contains("default: fast") && !short.contains("default: file"),
1695 "{short}"
1696 );
1697
1698 let long = render_help(&spec, &spec.cmd, true);
1699 assert!(long.contains("--long-only"), "{long}");
1700 assert!(!long.contains("--short-only"), "{long}");
1701 assert!(
1702 !long.contains("MODE") && !long.contains("possible values"),
1703 "{long}"
1704 );
1705
1706 let rendered = spec.to_string();
1707 let reparsed: crate::Spec = rendered.parse().unwrap();
1708 assert!(reparsed.cmd.flags[0].hide_default_value);
1709 assert!(reparsed.cmd.flags[0].hide_env);
1710 assert!(reparsed.cmd.flags[0].hide_possible_values);
1711 }
1712
1713 #[test]
1714 fn a_help_template_reorders_omits_and_wraps_the_sections() {
1715 let spec = crate::spec! { r#"
1719bin "ex"
1720about "An example"
1721help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1722flag "--force" help="Do it anyway"
1723arg "<file>" help="Which file"
1724cmd "run" help="Run it"
1725 "# }
1726 .unwrap();
1727
1728 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1729 An example
1730
1731 Usage: ex [--force] <file> [SUBCOMMAND]
1732
1733 Flags:
1734 --force Do it anyway
1735 -h, --help Print help
1736
1737 Arguments:
1738 <file> Which file
1739
1740 -- ask a person --
1741 ");
1742 }
1743
1744 #[test]
1745 fn a_template_places_the_sections_a_page_actually_has() {
1746 let spec = crate::spec! { r#"
1752bin "ex"
1753version "1.2.3"
1754about "An example"
1755after_help "Read the docs."
1756help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1757flag "--force" help="Do it anyway"
1758cmd "run" help="Run it"
1759 "# }
1760 .unwrap();
1761
1762 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1763 Usage: ex [--force] [SUBCOMMAND]
1764
1765 Flags:
1766 --force Do it anyway
1767 -h, --help Print help
1768 -V, --version Print version
1769
1770 Commands:
1771 run Run it
1772 help Print this message or the help of the given subcommand(s)
1773
1774 Read the docs.
1775
1776 ex 1.2.3
1777 An example
1778 ");
1779 }
1780
1781 #[test]
1782 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1783 let spec = crate::spec! { r#"
1786bin "ex"
1787flatten_help #true
1788help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1789cmd "run" help="Run it" {
1790 flag "--dry-run" help="Only show changes"
1791}
1792 "# }
1793 .unwrap();
1794
1795 let page = render_help(&spec, &spec.cmd, false);
1796 assert!(
1797 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1798 "{page}"
1799 );
1800 assert!(page.contains("--dry-run"), "{page}");
1801 }
1802
1803 #[test]
1804 fn test_render_help_with_before_after_help() {
1805 let spec = crate::spec! { r#"
1806bin "testcli"
1807before_help "This text appears before the help"
1808after_help "This text appears after the help"
1809flag "--verbose" help="Enable verbose output"
1810 "# }
1811 .unwrap();
1812
1813 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1814 This text appears before the help
1815
1816 Usage: testcli [--verbose]
1817
1818 Flags:
1819 --verbose Enable verbose output
1820 -h, --help Print help
1821
1822 This text appears after the help
1823 ");
1824 }
1825
1826 #[test]
1827 fn test_render_help_with_before_after_help_long() {
1828 let spec = crate::spec! { r#"
1829bin "testcli"
1830before_help "short before"
1831before_help_long "This is the long version of before help"
1832after_help "short after"
1833after_help_long "This is the long version of after help"
1834flag "--verbose" help="Enable verbose output"
1835 "# }
1836 .unwrap();
1837
1838 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1839 short before
1840
1841 Usage: testcli [--verbose]
1842
1843 Flags:
1844 --verbose Enable verbose output
1845 -h, --help Print help
1846
1847 short after
1848 ");
1849
1850 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1851 This is the long version of before help
1852
1853 Usage: testcli [--verbose]
1854
1855 Flags:
1856 --verbose Enable verbose output
1857 -h, --help Print help
1858
1859 This is the long version of after help
1860 ");
1861 }
1862
1863 #[test]
1864 fn root_help_stays_on_the_root_page() {
1865 let spec = crate::spec! { r#"
1866bin "testcli"
1867author "Root Author"
1868license "MIT"
1869before_help "root before"
1870before_help_long "root before long"
1871after_help "root after"
1872after_help_long "root after long"
1873example "testcli --verbose"
1874cmd "run" help="run it" {
1875 after_help "run after"
1876 after_long_help "run after long"
1877 example "testcli run"
1878 cmd "now" help="run it now"
1879}
1880 "# }
1881 .unwrap();
1882 let run = spec.cmd.subcommands.get("run").expect("run command");
1883 let now = run.subcommands.get("now").expect("now command");
1884
1885 for long in [false, true] {
1886 let page = render_help(&spec, now, long);
1887 for root_only in [
1888 "root before",
1889 "root after",
1890 "$ testcli --verbose",
1891 "run after",
1892 "$ testcli run",
1893 "Root Author",
1894 "License: MIT",
1895 ] {
1896 assert!(
1897 !page.contains(root_only),
1898 "long={long}, inherited ancestor metadata {root_only:?}:\n{page}"
1899 );
1900 }
1901 }
1902
1903 let short = render_help(&spec, run, false);
1904 let long = render_help(&spec, run, true);
1905 assert!(short.contains("run after"), "{short}");
1906 assert!(short.contains("$ testcli run"), "{short}");
1907 assert!(long.contains("run after long"), "{long}");
1908 assert!(long.contains("$ testcli run"), "{long}");
1909 }
1910
1911 #[test]
1912 fn test_render_help_with_examples() {
1913 let spec = crate::spec! { r#"
1914bin "testcli"
1915flag "--verbose" help="Enable verbose output"
1916example "testcli --verbose" header="Run with verbose output"
1917example "testcli" header="Run normally" help="Just runs the tool"
1918 "# }
1919 .unwrap();
1920
1921 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1922 Usage: testcli [--verbose]
1923
1924 Flags:
1925 --verbose Enable verbose output
1926 -h, --help Print help
1927
1928 Examples:
1929 Run with verbose output:
1930 $ testcli --verbose
1931
1932 Run normally:
1933 $ testcli
1934 ");
1935
1936 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1937 Usage: testcli [--verbose]
1938
1939 Flags:
1940 --verbose Enable verbose output
1941 -h, --help Print help
1942
1943 Examples:
1944 Run with verbose output:
1945 $ testcli --verbose
1946
1947 Run normally:
1948 Just runs the tool
1949 $ testcli
1950 ");
1951 }
1952
1953 #[test]
1954 fn test_render_help_with_version() {
1955 let spec = crate::spec! { r#"
1956bin "testcli"
1957name "TestCLI"
1958version "1.2.3"
1959flag "--verbose" help="Enable verbose output"
1960 "# }
1961 .unwrap();
1962
1963 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1964 TestCLI 1.2.3
1965 Usage: testcli [--verbose]
1966
1967 Flags:
1968 --verbose Enable verbose output
1969 -h, --help Print help
1970 -V, --version Print version
1971 ");
1972 }
1973
1974 #[test]
1975 fn test_render_help_with_only_long_version() {
1976 let spec = crate::spec! { r#"
1977bin "testcli"
1978long_version "1.2.3\ncommit abc123"
1979flag "--verbose" help="Enable verbose output"
1980 "# }
1981 .unwrap();
1982
1983 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1984 Usage: testcli [--verbose]
1985
1986 Flags:
1987 --verbose Enable verbose output
1988 -h, --help Print help
1989 -V, --version Print version
1990 ");
1991 }
1992
1993 #[test]
1994 fn test_render_help_omits_help_when_disabled() {
1995 let spec = crate::spec! { r#"
1999bin "testcli"
2000version "1.2.3"
2001disable_help #true
2002flag "--verbose" help="Enable verbose output"
2003 "# }
2004 .unwrap();
2005
2006 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2007 testcli 1.2.3
2008 Usage: testcli [--verbose]
2009
2010 Flags:
2011 --verbose Enable verbose output
2012 -V, --version Print version
2013 ");
2014 }
2015
2016 #[test]
2017 fn test_render_help_with_author_license() {
2018 let spec = crate::spec! { r#"
2019bin "testcli"
2020author "Test Author"
2021license "MIT"
2022flag "--verbose" help="Enable verbose output"
2023 "# }
2024 .unwrap();
2025
2026 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2028 Usage: testcli [--verbose]
2029
2030 Flags:
2031 --verbose Enable verbose output
2032 -h, --help Print help
2033 ");
2034
2035 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
2037 Usage: testcli [--verbose]
2038
2039 Flags:
2040 --verbose Enable verbose output
2041 -h, --help Print help
2042
2043 Author: Test Author
2044 License: MIT
2045 ");
2046 }
2047
2048 #[test]
2049 fn test_render_help_with_deprecated_command() {
2050 let spec = crate::spec! { r#"
2053bin "testcli"
2054term_width 80
2055flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
2056cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
2057cmd "new-cmd" help="Do something better"
2058 "# }
2059 .unwrap();
2060
2061 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2062 Usage: testcli [--old] [SUBCOMMAND]
2063
2064 Commands:
2065 new-cmd Do something better
2066 old-cmd Do something [deprecated: use new-cmd instead; warns at 6.2; removed
2067 at 7.0]
2068 help Print this message or the help of the given subcommand(s)
2069
2070 Flags:
2071 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
2072 -h, --help Print help
2073 ");
2074 }
2075
2076 #[test]
2077 fn deprecation_milestones_do_not_need_a_message() {
2078 let spec = crate::spec! { r#"
2079bin "testcli"
2080flag "--old" help="Old switch" deprecated_remove_at="7.0"
2081cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
2082 "# }
2083 .unwrap();
2084
2085 let page = render_help(&spec, &spec.cmd, false);
2086 assert!(
2087 page.contains("old-cmd Do something [deprecated: warns at 6.2]"),
2088 "{page}"
2089 );
2090 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
2091 assert!(!page.contains("[deprecated:;"), "{page}");
2092 }
2093
2094 #[test]
2095 fn test_render_help_with_subcommand_presentation() {
2096 let spec = crate::spec! { r#"
2097bin "testcli"
2098subcommand_help_heading "Actions"
2099subcommand_value_name "ACTION"
2100cmd "run" help="Run it\n"
2101 "# }
2102 .unwrap();
2103
2104 let page = render_help(&spec, &spec.cmd, false);
2105 assert!(page.contains("Usage: testcli [ACTION]"), "{page}");
2106 assert!(page.contains("\nActions:\n"), "{page}");
2107 }
2108
2109 #[test]
2110 fn test_render_help_honors_explicit_display_order() {
2111 let spec = crate::spec! { r#"
2112bin "testcli"
2113flag "--unset" help="Unordered"
2114flag "--later" help="Later" display_order=20
2115flag "--first" help="First" display_order=10
2116cmd "zulu" help="Unordered"
2117cmd "later" help="Later" display_order=20
2118cmd "first" help="First" display_order=10
2119cmd "alpha" help="Unordered"
2120 "# }
2121 .unwrap();
2122
2123 let page = render_help(&spec, &spec.cmd, false);
2124 let commands = page.split_once("\nCommands:\n").unwrap().1;
2125 assert!(
2126 commands.find("first").unwrap() < commands.find("later").unwrap()
2127 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
2128 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
2129 "{page}"
2130 );
2131 let flags = page.split_once("\nFlags:\n").unwrap().1;
2132 assert!(
2133 flags.find("--first").unwrap() < flags.find("--later").unwrap()
2134 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
2135 "{page}"
2136 );
2137 }
2138
2139 #[test]
2140 fn test_render_help_groups_subcommands_by_heading() {
2141 let spec = crate::spec! { r#"
2142bin "testcli"
2143cmd "run" help="Run it" help_heading="Core commands"
2144cmd "clean" help="Remove old state" help_heading="Maintenance"
2145cmd "status" help="Show status" help_heading="Commands"
2146 "# }
2147 .unwrap();
2148
2149 for page in [
2150 render_help(&spec, &spec.cmd, false),
2151 render_help(&spec, &spec.cmd, true),
2152 ] {
2153 let commands = page.find("\nCommands:\n").expect("default command section");
2154 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
2155 let core = page.find("\nCore commands:\n").expect("core section");
2156 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
2157 assert!(commands < core && commands < maintenance, "{page}");
2158 let default_end = core.min(maintenance);
2159 assert!(page[commands..default_end].contains("status"), "{page}");
2160 assert!(page[commands..default_end].contains("help"), "{page}");
2161 assert!(page[core..].contains("run"), "{page}");
2162 assert!(page[maintenance..].contains("clean"), "{page}");
2163 }
2164 }
2165
2166 #[test]
2167 fn test_render_help_with_next_line_layout() {
2168 let spec = crate::spec! { r#"
2169bin "testcli"
2170next_line_help #true
2171arg "<input>" help="Input file" env="INPUT" default="fast" {
2172 choices {
2173 choice "fast"
2174 choice "slow"
2175 }
2176}
2177flag "--verbose" help="Enable verbose output"
2178cmd "run" help="Run it"
2179 "# }
2180 .unwrap();
2181
2182 let short = render_help(&spec, &spec.cmd, false);
2183 assert!(!short.contains(" Run it\n\n help"), "{short}");
2184 for page in [short, render_help(&spec, &spec.cmd, true)] {
2185 assert!(page.contains(" [input]\n Input file"), "{page}");
2186 assert!(
2187 page.contains("--verbose\n Enable verbose output"),
2188 "{page}"
2189 );
2190 assert!(
2191 page.contains(
2192 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
2193 ),
2194 "{page}"
2195 );
2196 assert!(page.contains(" run\n Run it"), "{page}");
2197 }
2198 }
2199
2200 #[test]
2201 fn flatten_help_expands_subcommands_instead_of_listing_them() {
2202 let spec = crate::spec! { r#"
2203bin "testcli"
2204flatten_help #true
2205next_line_help #true
2206cmd "run" help="Run it" {
2207 arg "<task>" help="Task name" env="TASK" default="build" {
2208 choices {
2209 choice "build"
2210 choice "test"
2211 }
2212 }
2213 flag "--dry-run" help="Only show changes"
2214 flatten_help #true
2215 cmd "nested" help="Nested operation" {
2216 flag "--deep" help="Deep option"
2217 }
2218}
2219 "# }
2220 .unwrap();
2221
2222 for page in [
2223 render_help(&spec, &spec.cmd, false),
2224 render_help(&spec, &spec.cmd, true),
2225 ] {
2226 assert!(
2227 page.contains("Usage: testcli\n testcli run"),
2228 "{page}"
2229 );
2230 assert!(!page.contains("\nCommands:\n"), "{page}");
2231 assert!(page.contains("\nrun:\nRun it"), "{page}");
2232 assert!(page.contains("[task]"), "{page}");
2233 assert!(page.contains("--dry-run"), "{page}");
2234 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
2235 assert!(page.contains("--deep"), "{page}");
2236 assert!(
2237 page.contains(
2238 " [possible values: build, test]\n [env: TASK]\n (default: build)"
2239 ),
2240 "{page}"
2241 );
2242 }
2243 }
2244
2245 #[test]
2246 fn styled_help_colours_semantics_and_template_styles() {
2247 let mut spec = crate::spec! { r#"
2248bin "testcli"
2249help_template "{$bright-blue}My tool{/$}\n\n{{usage}}\n\n{{commands}}\n\n{{flags}}\n\n{{after_help}}"
2250cmd "build" help="Build it"
2251flag "--output <FILE>" help="Write **the file**"
2252 "# }
2253 .unwrap();
2254 spec.cmd.after_help_long = Some(
2255 "\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n \u{1b}[1mtestcli run\u{1b}[22m"
2256 .to_string(),
2257 );
2258
2259 let plain = render_help(&spec, &spec.cmd, true);
2260 assert!(plain.contains("My tool"), "{plain}");
2261 assert!(plain.contains("Examples:\n\n testcli run"), "{plain}");
2262 assert!(!plain.contains('\u{1b}'), "{plain:?}");
2263
2264 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2265 assert!(
2266 coloured.contains("\u{1b}[94mMy tool\u{1b}[0m"),
2267 "{coloured:?}"
2268 );
2269 assert!(
2270 coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"),
2271 "{coloured:?}"
2272 );
2273 assert!(
2274 coloured.contains("\u{1b}[1;32m--output\u{1b}[0m"),
2275 "{coloured:?}"
2276 );
2277 assert!(
2278 coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"),
2279 "{coloured:?}"
2280 );
2281 assert!(
2282 coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
2283 "{coloured:?}"
2284 );
2285 assert!(
2286 coloured.contains(
2287 "\u{1b}[1;32mhelp\u{1b}[0m Print this message or the help of the given subcommand(s)"
2288 ),
2289 "{coloured:?}"
2290 );
2291 assert!(
2292 coloured.contains("\u{1b}[1mthe file\u{1b}[22m"),
2293 "{coloured:?}"
2294 );
2295 assert!(
2296 coloured.contains("\u{1b}[1m\u{1b}[4mExamples:"),
2297 "{coloured:?}"
2298 );
2299 }
2300
2301 #[test]
2302 fn command_style_does_not_match_command_group_prose() {
2303 let spec = crate::spec! { r#"
2304bin "testcli"
2305heading "Build commands" help="build these projects before publishing"
2306cmd "build" help="Build it" help_heading="Build commands"
2307 "# }
2308 .unwrap();
2309
2310 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2311 assert!(
2312 coloured.contains(" build these projects before publishing"),
2313 "{coloured:?}"
2314 );
2315 assert!(
2316 coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
2317 "{coloured:?}"
2318 );
2319 assert!(
2320 !coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m these projects"),
2321 "{coloured:?}"
2322 );
2323 }
2324
2325 #[test]
2326 fn flattened_help_does_not_apply_command_style() {
2327 let spec = crate::spec! { r#"
2328bin "testcli"
2329flatten_help #true
2330help_template "Intro\n run should stay prose\n\n{{commands}}"
2331cmd "run" help="Run it"
2332 "# }
2333 .unwrap();
2334
2335 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2336 assert!(
2337 coloured.contains(" run should stay prose"),
2338 "{coloured:?}"
2339 );
2340 assert!(
2341 !coloured.contains("\u{1b}[1;32mrun\u{1b}[0m"),
2342 "{coloured:?}"
2343 );
2344 }
2345}