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 "indent_continuation",
1287 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1288 let value = value.as_str().unwrap_or("");
1289 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1290 let pad = " ".repeat(indent);
1291 let mut lines = value.split('\n');
1292 let mut out = lines.next().unwrap_or("").to_string();
1293 for line in lines {
1294 out.push('\n');
1295 if !line.is_empty() {
1296 out.push_str(&pad);
1297 out.push_str(line);
1298 }
1299 }
1300 Ok(out)
1301 },
1302 );
1303 tera.register_filter(
1304 "terminal_label",
1305 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1306 let value = value.as_str().unwrap_or("");
1307 let label = args.must_get::<String>("label")?;
1308 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1309 let indent = args.get::<u64>("indent")?.unwrap_or(0) as usize;
1310 Ok(crate::docs::layout::render_labelled_text(
1311 &label, value, width, indent,
1312 ))
1313 },
1314 );
1315 tera.register_filter(
1316 "terminal_annotation",
1317 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1318 let body = if let Some(values) = value.as_array() {
1319 values
1320 .iter()
1321 .filter_map(tera::Value::as_str)
1322 .collect::<Vec<_>>()
1323 .join(", ")
1324 } else {
1325 value.as_str().unwrap_or("").to_string()
1326 };
1327 let label = args.get::<String>("label")?.unwrap_or_default();
1328 let suffix = args.get::<String>("suffix")?.unwrap_or_default();
1329 let indent = args
1330 .get::<String>("indent")?
1331 .unwrap_or_default()
1332 .chars()
1333 .count();
1334 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1335 let rendered = crate::docs::layout::render_indented_text(
1336 &format!("{label}{body}{suffix}"),
1337 width,
1338 indent,
1339 );
1340 Ok(indent_text(&rendered, indent))
1341 },
1342 );
1343 tera.register_filter(
1344 "terminal_deprecation",
1345 |value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
1346 let field = |name| value.get_from_path(name).and_then(tera::Value::as_str);
1347 let Some(label) = deprecation_label(
1348 field("deprecated"),
1349 field("deprecated_warn_at"),
1350 field("deprecated_remove_at"),
1351 ) else {
1352 return Ok(String::new());
1353 };
1354 let indent = args
1355 .get::<String>("indent")?
1356 .unwrap_or_default()
1357 .chars()
1358 .count();
1359 let width = args.get::<u64>("width")?.unwrap_or(80) as usize;
1360 let rendered = crate::docs::layout::render_indented_text(&label, width, indent);
1361 Ok(indent_text(&rendered, indent))
1362 },
1363 );
1364
1365 #[rustfmt::skip]
1366 tera.add_raw_templates([
1367 ("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
1368 ("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
1369 ]).unwrap();
1370
1371 tera
1372});
1373
1374#[cfg(test)]
1375mod tests {
1376 use super::*;
1377 use insta::assert_snapshot;
1378
1379 #[test]
1380 fn flag_aliases_do_not_leak_into_interactive_help() {
1381 let spec = crate::spec! { r#"
1382bin "ex"
1383flag "-t -f --tail --follow" help="Follow output"
1384 "# }
1385 .unwrap();
1386
1387 for long in [false, true] {
1388 let page = super::render_help(&spec, &spec.cmd, long);
1389 assert!(page.contains("-t, --tail"), "long={long}:\n{page}");
1390 assert!(!page.contains("aliases:"), "long={long}:\n{page}");
1391 }
1392 }
1393
1394 #[test]
1395 fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
1396 let spec = crate::spec! { r#"
1401bin "ex"
1402flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
1403cmd inner help="a command" {
1404 flag "--plain" help="its own"
1405}
1406 "# }
1407 .unwrap();
1408
1409 let inner = spec.cmd.subcommands.get("inner").expect("inner");
1410 for long in [false, true] {
1411 let page = super::render_help(&spec, inner, long);
1412 assert!(
1413 !page.contains("--help"),
1414 "long={long}: a hidden ancestor binds this:\n{page}"
1415 );
1416 assert!(page.contains("-h"), "long={long}:\n{page}");
1418 }
1419 }
1420
1421 #[test]
1422 fn a_long_beats_a_negation_however_far_away_it_is() {
1423 let spec = crate::spec! { r#"
1429bin "ex"
1430flag "--no-cache" global=#true help="the root's plain long"
1431flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
1432cmd narrow help="a command" {
1433 flag "--cache" negate="--no-cache" help="its own, with a negation"
1434 flag "--tint" negate="--no-colour" help="claims the root's negation"
1435}
1436 "# }
1437 .unwrap();
1438
1439 let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
1440 for long in [false, true] {
1441 let page = super::render_help(&spec, narrow, long);
1442 assert!(
1443 page.contains("--no-cache"),
1444 "long={long}: a long beats a negation, so this still binds here:\n{page}"
1445 );
1446 assert!(page.contains("--colour"), "long={long}:\n{page}");
1449 let global = page
1450 .split_once("Global flags:")
1451 .expect("a global section")
1452 .1;
1453 assert!(
1454 !global.contains("--colour / --no-colour"),
1455 "long={long}: the nearer negation owns that spelling:\n{page}"
1456 );
1457 }
1458 }
1459
1460 #[test]
1461 fn a_description_of_only_spaces_is_no_description() {
1462 let spec = crate::spec! { r#"
1469bin "ex"
1470flag "--blank" help=" "
1471flag "--plain" help="plain"
1472 "# }
1473 .unwrap();
1474
1475 for long in [false, true] {
1476 let page = super::render_help(&spec, &spec.cmd, long);
1477 let listing = page.split_once("\nFlags:").expect("a flags section").1;
1480 let line = listing
1481 .lines()
1482 .find(|l| l.contains("--blank"))
1483 .unwrap_or_else(|| panic!("long={long}: {page}"));
1484 assert_eq!(
1485 line,
1486 line.trim_end(),
1487 "long={long}: trailing space on {line:?}"
1488 );
1489 }
1490 }
1491
1492 #[test]
1493 fn test_render_help_omits_hidden_entries() {
1494 let spec = crate::spec! { r#"
1495bin "ex"
1496flag "--visible" help="shown"
1497flag "--secret" hide=#true help="hidden"
1498flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
1499arg "[SHOWN]" help="an arg"
1500arg "[HIDDEN]" hide=#true help="a hidden arg"
1501cmd open help="a command"
1502cmd sneaky hide=#true help="a hidden command"
1503 "# }
1504 .unwrap();
1505
1506 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1511 Usage: ex [--visible] [SHOWN] [SUBCOMMAND]
1512
1513 Commands:
1514 open a command
1515 help Print this message or the help of the given subcommand(s)
1516
1517 Arguments:
1518 [SHOWN] an arg
1519
1520 Flags:
1521 --visible shown
1522 -h, --help Print help
1523 ");
1524 }
1525
1526 #[test]
1527 fn test_render_help_groups_by_heading() {
1528 let spec = crate::spec! { r#"
1529bin "testcli"
1530flag "--verbose" help="Verbose output"
1531flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1532flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
1533flag "--jobs <n>" help="How many at once" help_heading="Performance"
1534arg "<file>" help="The file"
1535arg "<mode>" help="How to run" help_heading="Behaviour"
1536 "# }
1537 .unwrap();
1538
1539 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1542 Usage: testcli [FLAGS] <file> <mode>
1543
1544 Arguments:
1545 <file> The file
1546
1547 Behaviour:
1548 <mode> How to run
1549
1550 Flags:
1551 --verbose Verbose output
1552 -h, --help Print help
1553
1554 Filtering:
1555 --filter <pattern> Only matching
1556 --exclude <pattern> Skip matching
1557
1558 Performance:
1559 --jobs <n> How many at once
1560 ");
1561 }
1562
1563 #[test]
1564 fn test_render_help_with_only_headed_flags() {
1565 let spec = crate::spec! { r#"
1568bin "testcli"
1569flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
1570 "# }
1571 .unwrap();
1572
1573 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1574 Usage: testcli [--filter <pattern>]
1575
1576 Flags:
1577 -h, --help Print help
1578
1579 Filtering:
1580 --filter <pattern> Only matching
1581 ");
1582 }
1583
1584 #[test]
1585 fn test_render_help_with_env() {
1586 let spec = crate::spec! { r#"
1587bin "testcli"
1588flag "--color" env="MYCLI_COLOR" help="Enable color output"
1589flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
1590flag "--debug" help="Debug mode"
1591 "# }
1592 .unwrap();
1593
1594 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1595 Usage: testcli [FLAGS]
1596
1597 Flags:
1598 --color Enable color output [env: MYCLI_COLOR]
1599 --verbose Verbose output [env: MYCLI_VERBOSE]
1600 --debug Debug mode
1601 -h, --help Print help
1602 ");
1603
1604 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1605 Usage: testcli [FLAGS]
1606
1607 Flags:
1608 --color Enable color output
1609 [env: MYCLI_COLOR]
1610 --verbose Verbose output
1611 [env: MYCLI_VERBOSE]
1612 --debug Debug mode
1613 -h, --help Print help
1614 ");
1615 }
1616
1617 #[test]
1618 fn test_render_help_with_arg_env() {
1619 let spec = crate::spec! { r#"
1620bin "testcli"
1621arg "<input>" env="MY_INPUT" help="Input file"
1622arg "<output>" env="MY_OUTPUT" help="Output file"
1623arg "<extra>" help="Extra arg without env"
1624arg "[default]" help="Arg with default value" default="default value"
1625 "# }
1626 .unwrap();
1627
1628 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1629 Usage: testcli <ARGS>…
1630
1631 Arguments:
1632 <input> Input file [env: MY_INPUT]
1633 <output> Output file [env: MY_OUTPUT]
1634 <extra> Extra arg without env
1635 [default] Arg with default value (default: default value)
1636
1637 Flags:
1638 -h, --help Print help
1639 ");
1640
1641 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1642 Usage: testcli <ARGS>…
1643
1644 Arguments:
1645 <input> Input file
1646 [env: MY_INPUT]
1647 <output> Output file
1648 [env: MY_OUTPUT]
1649 <extra> Extra arg without env
1650 [default] Arg with default value
1651 (default: default value)
1652
1653 Flags:
1654 -h, --help Print help
1655 ");
1656 }
1657
1658 #[test]
1659 fn test_render_help_with_negated_flag() {
1660 let spec = crate::spec! { r#"
1661bin "testcli"
1662flag "--compress" negate="--no-compress" default=#true help="Compress output"
1663flag "--verbose" help="Verbose output"
1664 "# }
1665 .unwrap();
1666
1667 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1668 Usage: testcli [--compress] [--verbose]
1669
1670 Flags:
1671 --compress / --no-compress Compress output (default: true)
1672 --verbose Verbose output
1673 -h, --help Print help
1674 ");
1675
1676 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1677 Usage: testcli [--compress] [--verbose]
1678
1679 Flags:
1680 --compress / --no-compress Compress output
1681 (default: true)
1682 --verbose Verbose output
1683 -h, --help Print help
1684 ");
1685 }
1686
1687 #[test]
1688 fn granular_help_hides_preserve_behavior_but_remove_presentation() {
1689 let spec = crate::spec! { r#"
1690bin "testcli"
1691flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
1692 choices {
1693 choice "fast"
1694 choice "slow"
1695 }
1696}
1697flag "--short-only" help="short" hide_long_help=#true
1698flag "--long-only" help="long" hide_short_help=#true
1699arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
1700 "# }
1701 .unwrap();
1702
1703 let short = render_help(&spec, &spec.cmd, false);
1704 assert!(short.contains("--mode <mode>"), "{short}");
1705 assert!(short.contains("--short-only"), "{short}");
1706 assert!(!short.contains("--long-only"), "{short}");
1707 assert!(
1708 !short.contains("MODE") && !short.contains("fast, slow"),
1709 "{short}"
1710 );
1711 assert!(
1712 !short.contains("default: fast") && !short.contains("default: file"),
1713 "{short}"
1714 );
1715
1716 let long = render_help(&spec, &spec.cmd, true);
1717 assert!(long.contains("--long-only"), "{long}");
1718 assert!(!long.contains("--short-only"), "{long}");
1719 assert!(
1720 !long.contains("MODE") && !long.contains("possible values"),
1721 "{long}"
1722 );
1723
1724 let rendered = spec.to_string();
1725 let reparsed: crate::Spec = rendered.parse().unwrap();
1726 assert!(reparsed.cmd.flags[0].hide_default_value);
1727 assert!(reparsed.cmd.flags[0].hide_env);
1728 assert!(reparsed.cmd.flags[0].hide_possible_values);
1729 }
1730
1731 #[test]
1732 fn a_help_template_reorders_omits_and_wraps_the_sections() {
1733 let spec = crate::spec! { r#"
1737bin "ex"
1738about "An example"
1739help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
1740flag "--force" help="Do it anyway"
1741arg "<file>" help="Which file"
1742cmd "run" help="Run it"
1743 "# }
1744 .unwrap();
1745
1746 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1747 An example
1748
1749 Usage: ex [--force] <file> [SUBCOMMAND]
1750
1751 Flags:
1752 --force Do it anyway
1753 -h, --help Print help
1754
1755 Arguments:
1756 <file> Which file
1757
1758 -- ask a person --
1759 ");
1760 }
1761
1762 #[test]
1763 fn a_template_places_the_sections_a_page_actually_has() {
1764 let spec = crate::spec! { r#"
1770bin "ex"
1771version "1.2.3"
1772about "An example"
1773after_help "Read the docs."
1774help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
1775flag "--force" help="Do it anyway"
1776cmd "run" help="Run it"
1777 "# }
1778 .unwrap();
1779
1780 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1781 Usage: ex [--force] [SUBCOMMAND]
1782
1783 Flags:
1784 --force Do it anyway
1785 -h, --help Print help
1786 -V, --version Print version
1787
1788 Commands:
1789 run Run it
1790 help Print this message or the help of the given subcommand(s)
1791
1792 Read the docs.
1793
1794 ex 1.2.3
1795 An example
1796 ");
1797 }
1798
1799 #[test]
1800 fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
1801 let spec = crate::spec! { r#"
1804bin "ex"
1805flatten_help #true
1806help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
1807cmd "run" help="Run it" {
1808 flag "--dry-run" help="Only show changes"
1809}
1810 "# }
1811 .unwrap();
1812
1813 let page = render_help(&spec, &spec.cmd, false);
1814 assert!(
1815 page.find("run:").unwrap() < page.find("Flags:").unwrap(),
1816 "{page}"
1817 );
1818 assert!(page.contains("--dry-run"), "{page}");
1819 }
1820
1821 #[test]
1822 fn test_render_help_with_before_after_help() {
1823 let spec = crate::spec! { r#"
1824bin "testcli"
1825before_help "This text appears before the help"
1826after_help "This text appears after the help"
1827flag "--verbose" help="Enable verbose output"
1828 "# }
1829 .unwrap();
1830
1831 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1832 This text appears before the help
1833
1834 Usage: testcli [--verbose]
1835
1836 Flags:
1837 --verbose Enable verbose output
1838 -h, --help Print help
1839
1840 This text appears after the help
1841 ");
1842 }
1843
1844 #[test]
1845 fn test_render_help_with_before_after_help_long() {
1846 let spec = crate::spec! { r#"
1847bin "testcli"
1848before_help "short before"
1849before_help_long "This is the long version of before help"
1850after_help "short after"
1851after_help_long "This is the long version of after help"
1852flag "--verbose" help="Enable verbose output"
1853 "# }
1854 .unwrap();
1855
1856 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1857 short before
1858
1859 Usage: testcli [--verbose]
1860
1861 Flags:
1862 --verbose Enable verbose output
1863 -h, --help Print help
1864
1865 short after
1866 ");
1867
1868 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1869 This is the long version of before help
1870
1871 Usage: testcli [--verbose]
1872
1873 Flags:
1874 --verbose Enable verbose output
1875 -h, --help Print help
1876
1877 This is the long version of after help
1878 ");
1879 }
1880
1881 #[test]
1882 fn root_help_stays_on_the_root_page() {
1883 let spec = crate::spec! { r#"
1884bin "testcli"
1885author "Root Author"
1886license "MIT"
1887before_help "root before"
1888before_help_long "root before long"
1889after_help "root after"
1890after_help_long "root after long"
1891example "testcli --verbose"
1892cmd "run" help="run it" {
1893 after_help "run after"
1894 after_long_help "run after long"
1895 example "testcli run"
1896 cmd "now" help="run it now"
1897}
1898 "# }
1899 .unwrap();
1900 let run = spec.cmd.subcommands.get("run").expect("run command");
1901 let now = run.subcommands.get("now").expect("now command");
1902
1903 for long in [false, true] {
1904 let page = render_help(&spec, now, long);
1905 for root_only in [
1906 "root before",
1907 "root after",
1908 "$ testcli --verbose",
1909 "run after",
1910 "$ testcli run",
1911 "Root Author",
1912 "License: MIT",
1913 ] {
1914 assert!(
1915 !page.contains(root_only),
1916 "long={long}, inherited ancestor metadata {root_only:?}:\n{page}"
1917 );
1918 }
1919 }
1920
1921 let short = render_help(&spec, run, false);
1922 let long = render_help(&spec, run, true);
1923 assert!(short.contains("run after"), "{short}");
1924 assert!(short.contains("$ testcli run"), "{short}");
1925 assert!(long.contains("run after long"), "{long}");
1926 assert!(long.contains("$ testcli run"), "{long}");
1927 }
1928
1929 #[test]
1930 fn test_render_help_indents_multiline_example() {
1931 let spec = crate::spec! { r##"
1932bin "testcli"
1933example #"""
1934testcli version
19351.2.3 linux
1936
1937built today
1938"""# help="Show the version"
1939cmd "version" {
1940 example #"""
1941testcli version
19421.2.3 linux
1943
1944built today
1945"""# help="Show the version"
1946}
1947 "## }
1948 .unwrap();
1949 let version = spec
1950 .cmd
1951 .subcommands
1952 .get("version")
1953 .expect("version command");
1954
1955 for long in [false, true] {
1956 for cmd in [&spec.cmd, version] {
1957 let page = render_help(&spec, cmd, long);
1958 assert!(
1959 page.contains(" $ testcli version\n 1.2.3 linux\n\n built today"),
1960 "long={long}:\n{page}"
1961 );
1962 }
1963 }
1964 }
1965
1966 #[test]
1967 fn test_render_help_with_examples() {
1968 let spec = crate::spec! { r#"
1969bin "testcli"
1970flag "--verbose" help="Enable verbose output"
1971example "testcli --verbose" header="Run with verbose output"
1972example "testcli" header="Run normally" help="Just runs the tool"
1973 "# }
1974 .unwrap();
1975
1976 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
1977 Usage: testcli [--verbose]
1978
1979 Flags:
1980 --verbose Enable verbose output
1981 -h, --help Print help
1982
1983 Examples:
1984 Run with verbose output:
1985 $ testcli --verbose
1986
1987 Run normally:
1988 $ testcli
1989 ");
1990
1991 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
1992 Usage: testcli [--verbose]
1993
1994 Flags:
1995 --verbose Enable verbose output
1996 -h, --help Print help
1997
1998 Examples:
1999 Run with verbose output:
2000 $ testcli --verbose
2001
2002 Run normally:
2003 Just runs the tool
2004 $ testcli
2005 ");
2006 }
2007
2008 #[test]
2009 fn test_render_help_with_version() {
2010 let spec = crate::spec! { r#"
2011bin "testcli"
2012name "TestCLI"
2013version "1.2.3"
2014flag "--verbose" help="Enable verbose output"
2015 "# }
2016 .unwrap();
2017
2018 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2019 TestCLI 1.2.3
2020 Usage: testcli [--verbose]
2021
2022 Flags:
2023 --verbose Enable verbose output
2024 -h, --help Print help
2025 -V, --version Print version
2026 ");
2027 }
2028
2029 #[test]
2030 fn test_render_help_with_only_long_version() {
2031 let spec = crate::spec! { r#"
2032bin "testcli"
2033long_version "1.2.3\ncommit abc123"
2034flag "--verbose" help="Enable verbose output"
2035 "# }
2036 .unwrap();
2037
2038 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2039 Usage: testcli [--verbose]
2040
2041 Flags:
2042 --verbose Enable verbose output
2043 -h, --help Print help
2044 -V, --version Print version
2045 ");
2046 }
2047
2048 #[test]
2049 fn test_render_help_omits_help_when_disabled() {
2050 let spec = crate::spec! { r#"
2054bin "testcli"
2055version "1.2.3"
2056disable_help #true
2057flag "--verbose" help="Enable verbose output"
2058 "# }
2059 .unwrap();
2060
2061 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2062 testcli 1.2.3
2063 Usage: testcli [--verbose]
2064
2065 Flags:
2066 --verbose Enable verbose output
2067 -V, --version Print version
2068 ");
2069 }
2070
2071 #[test]
2072 fn test_render_help_with_author_license() {
2073 let spec = crate::spec! { r#"
2074bin "testcli"
2075author "Test Author"
2076license "MIT"
2077flag "--verbose" help="Enable verbose output"
2078 "# }
2079 .unwrap();
2080
2081 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2083 Usage: testcli [--verbose]
2084
2085 Flags:
2086 --verbose Enable verbose output
2087 -h, --help Print help
2088 ");
2089
2090 assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
2092 Usage: testcli [--verbose]
2093
2094 Flags:
2095 --verbose Enable verbose output
2096 -h, --help Print help
2097
2098 Author: Test Author
2099 License: MIT
2100 ");
2101 }
2102
2103 #[test]
2104 fn test_render_help_with_deprecated_command() {
2105 let spec = crate::spec! { r#"
2108bin "testcli"
2109term_width 80
2110flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
2111cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
2112cmd "new-cmd" help="Do something better"
2113 "# }
2114 .unwrap();
2115
2116 assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
2117 Usage: testcli [--old] [SUBCOMMAND]
2118
2119 Commands:
2120 new-cmd Do something better
2121 old-cmd Do something [deprecated: use new-cmd instead; warns at 6.2; removed
2122 at 7.0]
2123 help Print this message or the help of the given subcommand(s)
2124
2125 Flags:
2126 --old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
2127 -h, --help Print help
2128 ");
2129 }
2130
2131 #[test]
2132 fn deprecation_milestones_do_not_need_a_message() {
2133 let spec = crate::spec! { r#"
2134bin "testcli"
2135flag "--old" help="Old switch" deprecated_remove_at="7.0"
2136cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
2137 "# }
2138 .unwrap();
2139
2140 let page = render_help(&spec, &spec.cmd, false);
2141 assert!(
2142 page.contains("old-cmd Do something [deprecated: warns at 6.2]"),
2143 "{page}"
2144 );
2145 assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
2146 assert!(!page.contains("[deprecated:;"), "{page}");
2147 }
2148
2149 #[test]
2150 fn test_render_help_with_subcommand_presentation() {
2151 let spec = crate::spec! { r#"
2152bin "testcli"
2153subcommand_help_heading "Actions"
2154subcommand_value_name "ACTION"
2155cmd "run" help="Run it\n"
2156 "# }
2157 .unwrap();
2158
2159 let page = render_help(&spec, &spec.cmd, false);
2160 assert!(page.contains("Usage: testcli [ACTION]"), "{page}");
2161 assert!(page.contains("\nActions:\n"), "{page}");
2162 }
2163
2164 #[test]
2165 fn test_render_help_honors_explicit_display_order() {
2166 let spec = crate::spec! { r#"
2167bin "testcli"
2168flag "--unset" help="Unordered"
2169flag "--later" help="Later" display_order=20
2170flag "--first" help="First" display_order=10
2171cmd "zulu" help="Unordered"
2172cmd "later" help="Later" display_order=20
2173cmd "first" help="First" display_order=10
2174cmd "alpha" help="Unordered"
2175 "# }
2176 .unwrap();
2177
2178 let page = render_help(&spec, &spec.cmd, false);
2179 let commands = page.split_once("\nCommands:\n").unwrap().1;
2180 assert!(
2181 commands.find("first").unwrap() < commands.find("later").unwrap()
2182 && commands.find("later").unwrap() < commands.find("alpha").unwrap()
2183 && commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
2184 "{page}"
2185 );
2186 let flags = page.split_once("\nFlags:\n").unwrap().1;
2187 assert!(
2188 flags.find("--first").unwrap() < flags.find("--later").unwrap()
2189 && flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
2190 "{page}"
2191 );
2192 }
2193
2194 #[test]
2195 fn test_render_help_groups_subcommands_by_heading() {
2196 let spec = crate::spec! { r#"
2197bin "testcli"
2198cmd "run" help="Run it" help_heading="Core commands"
2199cmd "clean" help="Remove old state" help_heading="Maintenance"
2200cmd "status" help="Show status" help_heading="Commands"
2201 "# }
2202 .unwrap();
2203
2204 for page in [
2205 render_help(&spec, &spec.cmd, false),
2206 render_help(&spec, &spec.cmd, true),
2207 ] {
2208 let commands = page.find("\nCommands:\n").expect("default command section");
2209 assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
2210 let core = page.find("\nCore commands:\n").expect("core section");
2211 let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
2212 assert!(commands < core && commands < maintenance, "{page}");
2213 let default_end = core.min(maintenance);
2214 assert!(page[commands..default_end].contains("status"), "{page}");
2215 assert!(page[commands..default_end].contains("help"), "{page}");
2216 assert!(page[core..].contains("run"), "{page}");
2217 assert!(page[maintenance..].contains("clean"), "{page}");
2218 }
2219 }
2220
2221 #[test]
2222 fn test_render_help_with_next_line_layout() {
2223 let spec = crate::spec! { r#"
2224bin "testcli"
2225next_line_help #true
2226arg "<input>" help="Input file" env="INPUT" default="fast" {
2227 choices {
2228 choice "fast"
2229 choice "slow"
2230 }
2231}
2232flag "--verbose" help="Enable verbose output"
2233cmd "run" help="Run it"
2234 "# }
2235 .unwrap();
2236
2237 let short = render_help(&spec, &spec.cmd, false);
2238 assert!(!short.contains(" Run it\n\n help"), "{short}");
2239 for page in [short, render_help(&spec, &spec.cmd, true)] {
2240 assert!(page.contains(" [input]\n Input file"), "{page}");
2241 assert!(
2242 page.contains("--verbose\n Enable verbose output"),
2243 "{page}"
2244 );
2245 assert!(
2246 page.contains(
2247 " [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
2248 ),
2249 "{page}"
2250 );
2251 assert!(page.contains(" run\n Run it"), "{page}");
2252 }
2253 }
2254
2255 #[test]
2256 fn flatten_help_expands_subcommands_instead_of_listing_them() {
2257 let spec = crate::spec! { r#"
2258bin "testcli"
2259flatten_help #true
2260next_line_help #true
2261cmd "run" help="Run it" {
2262 arg "<task>" help="Task name" env="TASK" default="build" {
2263 choices {
2264 choice "build"
2265 choice "test"
2266 }
2267 }
2268 flag "--dry-run" help="Only show changes"
2269 flatten_help #true
2270 cmd "nested" help="Nested operation" {
2271 flag "--deep" help="Deep option"
2272 }
2273}
2274 "# }
2275 .unwrap();
2276
2277 for page in [
2278 render_help(&spec, &spec.cmd, false),
2279 render_help(&spec, &spec.cmd, true),
2280 ] {
2281 assert!(
2282 page.contains("Usage: testcli\n testcli run"),
2283 "{page}"
2284 );
2285 assert!(!page.contains("\nCommands:\n"), "{page}");
2286 assert!(page.contains("\nrun:\nRun it"), "{page}");
2287 assert!(page.contains("[task]"), "{page}");
2288 assert!(page.contains("--dry-run"), "{page}");
2289 assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
2290 assert!(page.contains("--deep"), "{page}");
2291 assert!(
2292 page.contains(
2293 " [possible values: build, test]\n [env: TASK]\n (default: build)"
2294 ),
2295 "{page}"
2296 );
2297 }
2298 }
2299
2300 #[test]
2301 fn styled_help_colours_semantics_and_template_styles() {
2302 let mut spec = crate::spec! { r#"
2303bin "testcli"
2304help_template "{$bright-blue}My tool{/$}\n\n{{usage}}\n\n{{commands}}\n\n{{flags}}\n\n{{after_help}}"
2305cmd "build" help="Build it"
2306flag "--output <FILE>" help="Write **the file**"
2307 "# }
2308 .unwrap();
2309 spec.cmd.after_help_long = Some(
2310 "\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n \u{1b}[1mtestcli run\u{1b}[22m"
2311 .to_string(),
2312 );
2313
2314 let plain = render_help(&spec, &spec.cmd, true);
2315 assert!(plain.contains("My tool"), "{plain}");
2316 assert!(plain.contains("Examples:\n\n testcli run"), "{plain}");
2317 assert!(!plain.contains('\u{1b}'), "{plain:?}");
2318
2319 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2320 assert!(
2321 coloured.contains("\u{1b}[94mMy tool\u{1b}[0m"),
2322 "{coloured:?}"
2323 );
2324 assert!(
2325 coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"),
2326 "{coloured:?}"
2327 );
2328 assert!(
2329 coloured.contains("\u{1b}[1;32m--output\u{1b}[0m"),
2330 "{coloured:?}"
2331 );
2332 assert!(
2333 coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"),
2334 "{coloured:?}"
2335 );
2336 assert!(
2337 coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
2338 "{coloured:?}"
2339 );
2340 assert!(
2341 coloured.contains(
2342 "\u{1b}[1;32mhelp\u{1b}[0m Print this message or the help of the given subcommand(s)"
2343 ),
2344 "{coloured:?}"
2345 );
2346 assert!(
2347 coloured.contains("\u{1b}[1mthe file\u{1b}[22m"),
2348 "{coloured:?}"
2349 );
2350 assert!(
2351 coloured.contains("\u{1b}[1m\u{1b}[4mExamples:"),
2352 "{coloured:?}"
2353 );
2354 }
2355
2356 #[test]
2357 fn command_style_does_not_match_command_group_prose() {
2358 let spec = crate::spec! { r#"
2359bin "testcli"
2360heading "Build commands" help="build these projects before publishing"
2361cmd "build" help="Build it" help_heading="Build commands"
2362 "# }
2363 .unwrap();
2364
2365 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2366 assert!(
2367 coloured.contains(" build these projects before publishing"),
2368 "{coloured:?}"
2369 );
2370 assert!(
2371 coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
2372 "{coloured:?}"
2373 );
2374 assert!(
2375 !coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m these projects"),
2376 "{coloured:?}"
2377 );
2378 }
2379
2380 #[test]
2381 fn flattened_help_does_not_apply_command_style() {
2382 let spec = crate::spec! { r#"
2383bin "testcli"
2384flatten_help #true
2385help_template "Intro\n run should stay prose\n\n{{commands}}"
2386cmd "run" help="Run it"
2387 "# }
2388 .unwrap();
2389
2390 let coloured = render_help_styled(&spec, &spec.cmd, true, Style::COLOURED);
2391 assert!(
2392 coloured.contains(" run should stay prose"),
2393 "{coloured:?}"
2394 );
2395 assert!(
2396 !coloured.contains("\u{1b}[1;32mrun\u{1b}[0m"),
2397 "{coloured:?}"
2398 );
2399 }
2400}