use crate::{Spec, SpecCommand};
use std::sync::LazyLock;
use tera::Tera;
pub fn render_help(spec: &Spec, cmd: &SpecCommand, long: bool) -> String {
let docs_spec = crate::docs::models::Spec::from(spec.clone());
let mut docs_cmd = crate::docs::models::SpecCommand::from(&without_hidden(cmd, long));
let mut ctx = tera::Context::new();
ctx.insert("spec", &docs_spec);
ctx.insert("long", &long);
ctx.insert("root", &docs_cmd.full_cmd.is_empty());
ctx.insert("show_help_subcommand", &!cmd.disable_help_subcommand);
let (mut inherited, ancestors_taken) = inherited_flags(spec, cmd, &docs_cmd.full_cmd, long);
{
let supplied = supplied_flags(spec, cmd, &ancestors_taken, docs_cmd.full_cmd.is_empty());
if !supplied.is_empty() {
match docs_cmd
.flag_groups
.iter_mut()
.find(|g| g.heading.is_none())
{
Some(group) => group.items.extend(supplied),
None => docs_cmd.flag_groups.insert(
0,
crate::docs::models::Group {
heading: None,
items: supplied,
},
),
}
}
}
let width = crate::docs::layout::help_width(cmd.term_width, cmd.max_term_width);
let col = crate::docs::layout::max_usage_width(
docs_cmd
.flag_groups
.iter()
.flat_map(|g| g.items.iter())
.chain(inherited.iter())
.map(|f| f.display_usage.as_str()),
);
for group in &mut docs_cmd.flag_groups {
lay_out(&mut group.items, width, col);
}
lay_out(&mut inherited, width, col);
ctx.insert("cmd", &docs_cmd);
ctx.insert("global_flags", &inherited);
for (name, mark) in MARKS {
ctx.insert(name, &mark);
}
let template = if long {
"spec_template_long.tera"
} else {
"spec_template_short.tera"
};
let rendered = TERA.render(template, &ctx).unwrap();
let sections = Sections::split(&rendered);
let page = match spec
.help_template
.as_deref()
.filter(|t| crate::help_template::is_set(t))
{
Some(template) => crate::help_template::substitute(template, |name| sections.named(name)),
None => sections.concatenated(),
};
page.trim().to_string() + "\n"
}
const MARKS: [(&str, &str); 6] = [
("mark_usage", "\u{1}usage\u{1}"),
("mark_commands", "\u{1}commands\u{1}"),
("mark_args", "\u{1}args\u{1}"),
("mark_flags", "\u{1}flags\u{1}"),
("mark_flattened", "\u{1}flattened\u{1}"),
("mark_after_help", "\u{1}after_help\u{1}"),
];
struct Sections<'a> {
about: &'a str,
usage: &'a str,
commands: &'a str,
args: &'a str,
flags: &'a str,
flattened: &'a str,
after_help: &'a str,
}
impl<'a> Sections<'a> {
fn split(rendered: &'a str) -> Self {
let mut rest = rendered;
let mut parts: Vec<&str> = Vec::with_capacity(MARKS.len() + 1);
for (_, mark) in MARKS {
match rest.split_once(mark) {
Some((before, after)) => {
parts.push(before);
rest = after;
}
None => parts.push(""),
}
}
parts.push(rest);
Self {
about: parts[0],
usage: parts[1],
commands: parts[2],
args: parts[3],
flags: parts[4],
flattened: parts[5],
after_help: parts[6],
}
}
fn concatenated(&self) -> String {
[
self.about,
self.usage,
self.commands,
self.args,
self.flags,
self.flattened,
self.after_help,
]
.concat()
}
fn named(&self, name: &str) -> Option<String> {
Some(match name {
"about" => self.about.trim().to_string(),
"usage" => self.usage.trim().to_string(),
"commands" => {
let mut out = self.commands.trim().to_string();
let flattened = self.flattened.trim();
if !flattened.is_empty() {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str(flattened);
}
out
}
"args" => self.args.trim().to_string(),
"flags" => self.flags.trim().to_string(),
"after_help" => self.after_help.trim().to_string(),
_ => return None,
})
}
}
fn supplied_flags(
spec: &Spec,
cmd: &SpecCommand,
ancestors_taken: &[String],
is_root: bool,
) -> Vec<crate::docs::models::SpecFlag> {
let mut taken: Vec<String> = ancestors_taken.to_vec();
for f in &cmd.flags {
taken.extend(f.long.iter().map(|l| format!("--{l}")));
taken.extend(f.short.iter().map(|s| format!("-{s}")));
taken.extend(f.negate.clone());
}
let build = |name: &str, long: &str, short: char, help: &str| {
let long_free = !taken.contains(&format!("--{long}"));
let short_free = !taken.contains(&format!("-{short}"));
if !long_free && !short_free {
return None;
}
let name = if long_free { name } else { &short.to_string() };
let mut flag = crate::SpecFlag {
name: name.to_string(),
long: if long_free {
vec![long.to_string()]
} else {
vec![]
},
short: if short_free { vec![short] } else { vec![] },
help: Some(help.to_string()),
..Default::default()
};
flag.usage = flag.usage();
Some(crate::docs::models::SpecFlag::from(&flag))
};
let mut out = Vec::new();
if spec.disable_help != Some(true) && !cmd.disable_help_flag {
out.extend(build("help", "help", 'h', "Print help"));
}
if is_root
&& (spec.version.is_some() || spec.long_version.is_some())
&& !cmd.disable_version_flag
{
out.extend(build("version", "version", 'V', "Print version"));
}
out
}
fn lay_out(flags: &mut [crate::docs::models::SpecFlag], terminal_width: usize, col: usize) {
for flag in flags {
flag.usage_col_width = col;
flag.help_rendered = None;
flag.help_is_multiline = false;
let help = flag.help_long.as_deref().or(flag.help.as_deref());
if let Some(help) = help {
let (rendered, is_multiline) =
crate::docs::layout::render_help_text(help, terminal_width, col);
if !rendered.is_empty() {
flag.help_rendered = Some(rendered);
flag.help_is_multiline = is_multiline;
}
}
}
}
fn inherited_flags(
spec: &Spec,
cmd: &SpecCommand,
full_cmd: &[String],
long_help: bool,
) -> (Vec<crate::docs::models::SpecFlag>, Vec<String>) {
let mut ancestors: Vec<&SpecCommand> = Vec::new();
let mut at = &spec.cmd;
for name in full_cmd.iter().take(full_cmd.len().saturating_sub(1)) {
ancestors.push(at);
let Some(next) = at.subcommands.get(name) else {
return (Vec::new(), Vec::new());
};
at = next;
}
if !full_cmd.is_empty() {
ancestors.push(at);
}
let forms = |f: &crate::SpecFlag| -> Vec<String> {
f.long
.iter()
.map(|l| format!("--{l}"))
.chain(f.short.iter().map(|s| format!("-{s}")))
.collect()
};
let every_form: Vec<String> = cmd
.flags
.iter()
.chain(
ancestors
.iter()
.flat_map(|a| a.flags.iter())
.filter(|f| f.global),
)
.flat_map(&forms)
.collect();
let mut taken: Vec<String> = cmd.flags.iter().flat_map(&forms).collect();
let mut taken_negations: Vec<String> =
cmd.flags.iter().filter_map(|f| f.negate.clone()).collect();
let mut keep: Vec<(&crate::SpecFlag, Option<String>, Option<char>, bool)> = Vec::new();
for ancestor in ancestors.iter().rev() {
for f in ancestor.flags.iter().filter(|f| f.global) {
let long = f
.long
.iter()
.find(|l| !f.hidden_aliases.contains(l) && !taken.contains(&format!("--{l}")))
.cloned();
let short = f
.short
.iter()
.find(|s| !f.hidden_short_aliases.contains(s) && !taken.contains(&format!("-{s}")))
.copied();
let mine = forms(f);
let negate = f.negate.as_ref().is_some_and(|n| {
!taken_negations.contains(n) && (!every_form.contains(n) || mine.contains(n))
});
taken.extend(forms(f));
taken_negations.extend(f.negate.clone());
if f.hide
|| if long_help {
f.hide_long_help
} else {
f.hide_short_help
}
|| (long.is_none() && short.is_none() && !negate)
{
continue;
}
keep.push((f, long, short, negate));
}
}
let shown: Vec<crate::docs::models::SpecFlag> = ancestors
.iter()
.flat_map(|a| a.flags.iter())
.filter_map(|f| {
keep.iter()
.find(|(k, _, _, _)| std::ptr::eq(*k, f))
.map(|(_, l, s, n)| (f, l.clone(), *s, *n))
})
.map(|(f, long, short, negate)| {
let mut shown = f.clone();
shown.long = long.into_iter().collect();
shown.short = short.into_iter().collect();
if !negate {
shown.negate = None;
}
shown.usage = shown.usage();
crate::docs::models::SpecFlag::from(&shown)
})
.collect();
taken.extend(taken_negations);
(shown, taken)
}
fn without_hidden(cmd: &SpecCommand, long: bool) -> SpecCommand {
let mut visible = cmd.clone();
visible.flags.retain(|flag| {
!flag.hide
&& if long {
!flag.hide_long_help
} else {
!flag.hide_short_help
}
});
visible.args.retain(|arg| {
!arg.hide
&& if long {
!arg.hide_long_help
} else {
!arg.hide_short_help
}
});
visible.subcommands.retain(|_, sub| !sub.hide);
if visible.flatten_help {
for sub in visible.subcommands.values_mut() {
*sub = without_hidden(sub, long);
}
}
visible
}
static TERA: LazyLock<Tera> = LazyLock::new(|| {
let mut tera = Tera::default();
tera.register_filter(
"ljust",
|value: &tera::Value, args: tera::Kwargs, _: &tera::State| -> tera::TeraResult<String> {
let value = value.as_str().unwrap_or("");
let width = args.get::<u64>("width")?.unwrap_or(0) as usize;
Ok(format!("{:<width$}", value, width = width))
},
);
tera.register_filter(
"default",
|value: &tera::Value,
kwargs: tera::Kwargs,
_: &tera::State|
-> tera::TeraResult<tera::Value> {
let default_val = kwargs.must_get::<tera::Value>("value")?;
let boolean = kwargs.get::<bool>("boolean")?.unwrap_or_default();
if value.is_undefined() || value.is_none() || (boolean && !value.is_truthy()) {
Ok(default_val)
} else {
Ok(value.clone())
}
},
);
#[rustfmt::skip]
tera.add_raw_templates([
("spec_template_short.tera", include_str!("templates/spec_template_short.tera")),
("spec_template_long.tera", include_str!("templates/spec_template_long.tera")),
]).unwrap();
tera
});
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
#[test]
fn a_hidden_ancestor_claim_keeps_help_off_the_page() {
let spec = crate::spec! { r#"
bin "ex"
flag "--help" global=#true hide=#true help="the CLI's own, and invisible"
cmd inner help="a command" {
flag "--plain" help="its own"
}
"# }
.unwrap();
let inner = spec.cmd.subcommands.get("inner").expect("inner");
for long in [false, true] {
let page = super::render_help(&spec, inner, long);
assert!(
!page.contains("--help"),
"long={long}: a hidden ancestor binds this:\n{page}"
);
assert!(page.contains("-h"), "long={long}:\n{page}");
}
}
#[test]
fn a_long_beats_a_negation_however_far_away_it_is() {
let spec = crate::spec! { r#"
bin "ex"
flag "--no-cache" global=#true help="the root's plain long"
flag "--colour" negate="--no-colour" global=#true help="the root's, with a negation"
cmd narrow help="a command" {
flag "--cache" negate="--no-cache" help="its own, with a negation"
flag "--tint" negate="--no-colour" help="claims the root's negation"
}
"# }
.unwrap();
let narrow = spec.cmd.subcommands.get("narrow").expect("narrow");
for long in [false, true] {
let page = super::render_help(&spec, narrow, long);
assert!(
page.contains("--no-cache"),
"long={long}: a long beats a negation, so this still binds here:\n{page}"
);
assert!(page.contains("--colour"), "long={long}:\n{page}");
let global = page
.split_once("Global flags:")
.expect("a global section")
.1;
assert!(
!global.contains("--colour / --no-colour"),
"long={long}: the nearer negation owns that spelling:\n{page}"
);
}
}
#[test]
fn a_description_of_only_spaces_is_no_description() {
let spec = crate::spec! { r#"
bin "ex"
flag "--blank" help=" "
flag "--plain" help="plain"
"# }
.unwrap();
for long in [false, true] {
let page = super::render_help(&spec, &spec.cmd, long);
let listing = page.split_once("\nFlags:").expect("a flags section").1;
let line = listing
.lines()
.find(|l| l.contains("--blank"))
.unwrap_or_else(|| panic!("long={long}: {page}"));
assert_eq!(
line,
line.trim_end(),
"long={long}: trailing space on {line:?}"
);
}
}
#[test]
fn test_render_help_omits_hidden_entries() {
let spec = crate::spec! { r#"
bin "ex"
flag "--visible" help="shown"
flag "--secret" hide=#true help="hidden"
flag "--filtered" hide=#true help="hidden" help_heading="Filtering"
arg "[SHOWN]" help="an arg"
arg "[HIDDEN]" hide=#true help="a hidden arg"
cmd open help="a command"
cmd sneaky hide=#true help="a hidden command"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: ex [--visible] [SHOWN] <SUBCOMMAND>
Commands:
open a command
help Print this message or the help of the given subcommand(s)
Arguments:
[SHOWN] an arg
Flags:
--visible shown
-h, --help Print help
");
}
#[test]
fn test_render_help_groups_by_heading() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--verbose" help="Verbose output"
flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
flag "--exclude <pattern>" help="Skip matching" help_heading="Filtering"
flag "--jobs <n>" help="How many at once" help_heading="Performance"
arg "<file>" help="The file"
arg "<mode>" help="How to run" help_heading="Behaviour"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [FLAGS] <file> <mode>
Arguments:
<file> The file
Behaviour:
<mode> How to run
Flags:
--verbose Verbose output
-h, --help Print help
Filtering:
--filter <pattern> Only matching
--exclude <pattern> Skip matching
Performance:
--jobs <n> How many at once
");
}
#[test]
fn test_render_help_with_only_headed_flags() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [--filter <pattern>]
Flags:
-h, --help Print help
Filtering:
--filter <pattern> Only matching
");
}
#[test]
fn test_render_help_with_env() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--color" env="MYCLI_COLOR" help="Enable color output"
flag "--verbose" env="MYCLI_VERBOSE" help="Verbose output"
flag "--debug" help="Debug mode"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [FLAGS]
Flags:
--color Enable color output [env: MYCLI_COLOR]
--verbose Verbose output [env: MYCLI_VERBOSE]
--debug Debug mode
-h, --help Print help
");
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
Usage: testcli [FLAGS]
Flags:
--color Enable color output
[env: MYCLI_COLOR]
--verbose Verbose output
[env: MYCLI_VERBOSE]
--debug Debug mode
-h, --help Print help
");
}
#[test]
fn test_render_help_with_arg_env() {
let spec = crate::spec! { r#"
bin "testcli"
arg "<input>" env="MY_INPUT" help="Input file"
arg "<output>" env="MY_OUTPUT" help="Output file"
arg "<extra>" help="Extra arg without env"
arg "[default]" help="Arg with default value" default="default value"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli <ARGS>…
Arguments:
<input> Input file [env: MY_INPUT]
<output> Output file [env: MY_OUTPUT]
<extra> Extra arg without env
[default] Arg with default value (default: default value)
Flags:
-h, --help Print help
");
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
Usage: testcli <ARGS>…
Arguments:
<input> Input file
[env: MY_INPUT]
<output> Output file
[env: MY_OUTPUT]
<extra> Extra arg without env
[default] Arg with default value
(default: default value)
Flags:
-h, --help Print help
");
}
#[test]
fn test_render_help_with_negated_flag() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--compress" negate="--no-compress" default=#true help="Compress output"
flag "--verbose" help="Verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [--compress] [--verbose]
Flags:
--compress / --no-compress Compress output (default: true)
--verbose Verbose output
-h, --help Print help
");
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
Usage: testcli [--compress] [--verbose]
Flags:
--compress / --no-compress Compress output
(default: true)
--verbose Verbose output
-h, --help Print help
");
}
#[test]
fn granular_help_hides_preserve_behavior_but_remove_presentation() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--mode <mode>" help="Select mode" env="MODE" default="fast" hide_default_value=#true hide_env=#true hide_possible_values=#true {
choices {
choice "fast"
choice "slow"
}
}
flag "--short-only" help="short" hide_long_help=#true
flag "--long-only" help="long" hide_short_help=#true
arg "[input]" help="Input" env="INPUT" default="file" hide_default_value=#true hide_env=#true
"# }
.unwrap();
let short = render_help(&spec, &spec.cmd, false);
assert!(short.contains("--mode <mode>"), "{short}");
assert!(short.contains("--short-only"), "{short}");
assert!(!short.contains("--long-only"), "{short}");
assert!(
!short.contains("MODE") && !short.contains("fast, slow"),
"{short}"
);
assert!(
!short.contains("default: fast") && !short.contains("default: file"),
"{short}"
);
let long = render_help(&spec, &spec.cmd, true);
assert!(long.contains("--long-only"), "{long}");
assert!(!long.contains("--short-only"), "{long}");
assert!(
!long.contains("MODE") && !long.contains("possible values"),
"{long}"
);
let rendered = spec.to_string();
let reparsed: crate::Spec = rendered.parse().unwrap();
assert!(reparsed.cmd.flags[0].hide_default_value);
assert!(reparsed.cmd.flags[0].hide_env);
assert!(reparsed.cmd.flags[0].hide_possible_values);
}
#[test]
fn a_help_template_reorders_omits_and_wraps_the_sections() {
let spec = crate::spec! { r#"
bin "ex"
about "An example"
help_template "{{about}}\n\n{{usage}}\n\n{{flags}}\n\n{{args}}\n\n-- ask a person --"
flag "--force" help="Do it anyway"
arg "<file>" help="Which file"
cmd "run" help="Run it"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
An example
Usage: ex [--force] <file> <SUBCOMMAND>
Flags:
--force Do it anyway
-h, --help Print help
Arguments:
<file> Which file
-- ask a person --
");
}
#[test]
fn a_template_places_the_sections_a_page_actually_has() {
let spec = crate::spec! { r#"
bin "ex"
version "1.2.3"
about "An example"
after_help "Read the docs."
help_template "{{usage}}\n\n{{flags}}\n\n{{args}}\n\n{{commands}}\n\n{{after_help}}\n\n{{about}}"
flag "--force" help="Do it anyway"
cmd "run" help="Run it"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
Usage: ex [--force] <SUBCOMMAND>
Flags:
--force Do it anyway
-h, --help Print help
-V, --version Print version
Commands:
run
Run it
help
Print this message or the help of the given subcommand(s)
Read the docs.
ex 1.2.3
An example
");
}
#[test]
fn a_flattened_page_puts_its_bodies_where_the_commands_would_go() {
let spec = crate::spec! { r#"
bin "ex"
flatten_help #true
help_template "{{usage}}\n\n{{commands}}\n\n{{flags}}"
cmd "run" help="Run it" {
flag "--dry-run" help="Only show changes"
}
"# }
.unwrap();
let page = render_help(&spec, &spec.cmd, false);
assert!(
page.find("run:").unwrap() < page.find("Flags:").unwrap(),
"{page}"
);
assert!(page.contains("--dry-run"), "{page}");
}
#[test]
fn test_render_help_with_before_after_help() {
let spec = crate::spec! { r#"
bin "testcli"
before_help "This text appears before the help"
after_help "This text appears after the help"
flag "--verbose" help="Enable verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
This text appears before the help
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
This text appears after the help
");
}
#[test]
fn test_render_help_with_before_after_help_long() {
let spec = crate::spec! { r#"
bin "testcli"
before_help "short before"
before_help_long "This is the long version of before help"
after_help "short after"
after_help_long "This is the long version of after help"
flag "--verbose" help="Enable verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
short before
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
short after
");
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
This is the long version of before help
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
This is the long version of after help
");
}
#[test]
fn test_render_help_with_examples() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--verbose" help="Enable verbose output"
example "testcli --verbose" header="Run with verbose output"
example "testcli" header="Run normally" help="Just runs the tool"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
Examples:
Run with verbose output:
$ testcli --verbose
Run normally:
$ testcli
");
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
Examples:
Run with verbose output:
$ testcli --verbose
Run normally:
Just runs the tool
$ testcli
");
}
#[test]
fn test_render_help_with_version() {
let spec = crate::spec! { r#"
bin "testcli"
name "TestCLI"
version "1.2.3"
flag "--verbose" help="Enable verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
TestCLI 1.2.3
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
-V, --version Print version
");
}
#[test]
fn test_render_help_with_only_long_version() {
let spec = crate::spec! { r#"
bin "testcli"
long_version "1.2.3\ncommit abc123"
flag "--verbose" help="Enable verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
-V, --version Print version
");
}
#[test]
fn test_render_help_omits_help_when_disabled() {
let spec = crate::spec! { r#"
bin "testcli"
version "1.2.3"
disable_help #true
flag "--verbose" help="Enable verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
testcli 1.2.3
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-V, --version Print version
");
}
#[test]
fn test_render_help_with_author_license() {
let spec = crate::spec! { r#"
bin "testcli"
author "Test Author"
license "MIT"
flag "--verbose" help="Enable verbose output"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
");
assert_snapshot!(render_help(&spec, &spec.cmd, true), @"
Usage: testcli [--verbose]
Flags:
--verbose Enable verbose output
-h, --help Print help
Author: Test Author
License: MIT
");
}
#[test]
fn test_render_help_with_deprecated_command() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--old" help="Old switch" deprecated="use --new" deprecated_warn_at="6.1" deprecated_remove_at="7.0"
cmd "old-cmd" help="Do something" deprecated="use new-cmd instead" deprecated_warn_at="6.2" deprecated_remove_at="7.0"
cmd "new-cmd" help="Do something better"
"# }
.unwrap();
assert_snapshot!(render_help(&spec, &spec.cmd, false), @"
Usage: testcli [--old] <SUBCOMMAND>
Commands:
new-cmd Do something better
old-cmd [deprecated: use new-cmd instead; warns at 6.2; removed at 7.0] Do something
help Print this message or the help of the given subcommand(s)
Flags:
--old Old switch [deprecated: use --new; warns at 6.1; removed at 7.0]
-h, --help Print help
");
}
#[test]
fn deprecation_milestones_do_not_need_a_message() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--old" help="Old switch" deprecated_remove_at="7.0"
cmd "old-cmd" help="Do something" deprecated_warn_at="6.2"
"# }
.unwrap();
let page = render_help(&spec, &spec.cmd, false);
assert!(
page.contains("old-cmd [deprecated: warns at 6.2]"),
"{page}"
);
assert!(page.contains("[deprecated: removed at 7.0]"), "{page}");
assert!(!page.contains("[deprecated:;"), "{page}");
}
#[test]
fn test_render_help_with_subcommand_presentation() {
let spec = crate::spec! { r#"
bin "testcli"
subcommand_help_heading "Actions"
subcommand_value_name "ACTION"
cmd "run" help="Run it\n"
"# }
.unwrap();
let page = render_help(&spec, &spec.cmd, false);
assert!(page.contains("Usage: testcli <ACTION>"), "{page}");
assert!(page.contains("\nActions:\n"), "{page}");
}
#[test]
fn test_render_help_honors_explicit_display_order() {
let spec = crate::spec! { r#"
bin "testcli"
flag "--unset" help="Unordered"
flag "--later" help="Later" display_order=20
flag "--first" help="First" display_order=10
cmd "zulu" help="Unordered"
cmd "later" help="Later" display_order=20
cmd "first" help="First" display_order=10
cmd "alpha" help="Unordered"
"# }
.unwrap();
let page = render_help(&spec, &spec.cmd, false);
let commands = page.split_once("\nCommands:\n").unwrap().1;
assert!(
commands.find("first").unwrap() < commands.find("later").unwrap()
&& commands.find("later").unwrap() < commands.find("alpha").unwrap()
&& commands.find("alpha").unwrap() < commands.find("zulu").unwrap(),
"{page}"
);
let flags = page.split_once("\nFlags:\n").unwrap().1;
assert!(
flags.find("--first").unwrap() < flags.find("--later").unwrap()
&& flags.find("--later").unwrap() < flags.find("--unset").unwrap(),
"{page}"
);
}
#[test]
fn test_render_help_groups_subcommands_by_heading() {
let spec = crate::spec! { r#"
bin "testcli"
cmd "run" help="Run it" help_heading="Core commands"
cmd "clean" help="Remove old state" help_heading="Maintenance"
cmd "status" help="Show status" help_heading="Commands"
"# }
.unwrap();
for page in [
render_help(&spec, &spec.cmd, false),
render_help(&spec, &spec.cmd, true),
] {
let commands = page.find("\nCommands:\n").expect("default command section");
assert_eq!(page.matches("\nCommands:\n").count(), 1, "{page}");
let core = page.find("\nCore commands:\n").expect("core section");
let maintenance = page.find("\nMaintenance:\n").expect("maintenance section");
assert!(commands < core && commands < maintenance, "{page}");
let default_end = core.min(maintenance);
assert!(page[commands..default_end].contains("status"), "{page}");
assert!(page[commands..default_end].contains("help"), "{page}");
assert!(page[core..].contains("run"), "{page}");
assert!(page[maintenance..].contains("clean"), "{page}");
}
}
#[test]
fn test_render_help_with_next_line_layout() {
let spec = crate::spec! { r#"
bin "testcli"
next_line_help #true
arg "<input>" help="Input file" env="INPUT" default="fast" {
choices {
choice "fast"
choice "slow"
}
}
flag "--verbose" help="Enable verbose output"
cmd "run" help="Run it"
"# }
.unwrap();
let short = render_help(&spec, &spec.cmd, false);
assert!(!short.contains(" Run it\n\n help"), "{short}");
for page in [short, render_help(&spec, &spec.cmd, true)] {
assert!(page.contains(" [input]\n Input file"), "{page}");
assert!(
page.contains("--verbose\n Enable verbose output"),
"{page}"
);
assert!(
page.contains(
" [possible values: fast, slow]\n [env: INPUT]\n (default: fast)"
),
"{page}"
);
assert!(page.contains(" run\n Run it"), "{page}");
}
}
#[test]
fn flatten_help_expands_subcommands_instead_of_listing_them() {
let spec = crate::spec! { r#"
bin "testcli"
flatten_help #true
next_line_help #true
cmd "run" help="Run it" {
arg "<task>" help="Task name" env="TASK" default="build" {
choices {
choice "build"
choice "test"
}
}
flag "--dry-run" help="Only show changes"
flatten_help #true
cmd "nested" help="Nested operation" {
flag "--deep" help="Deep option"
}
}
"# }
.unwrap();
for page in [
render_help(&spec, &spec.cmd, false),
render_help(&spec, &spec.cmd, true),
] {
assert!(
page.contains("Usage: testcli\n testcli run"),
"{page}"
);
assert!(!page.contains("\nCommands:\n"), "{page}");
assert!(page.contains("\nrun:\nRun it"), "{page}");
assert!(page.contains("[task]"), "{page}");
assert!(page.contains("--dry-run"), "{page}");
assert!(page.contains("\nrun nested:\nNested operation"), "{page}");
assert!(page.contains("--deep"), "{page}");
assert!(
page.contains(
" [possible values: build, test]\n [env: TASK]\n (default: build)"
),
"{page}"
);
}
}
}