use core::fmt::Write as _;
use std::borrow::Cow;
use crate::spec::{
AdmonitionKind, AdmonitionMeta, ArgMeta, CommandMeta, Example, FlagMeta, Spec, ViewMeta,
};
use crate::Command;
use crate::DoubleDash;
mod template;
pub use template::STYLES;
const BLOCK_INDENT: usize = 4;
const MIN_INLINE_HELP_WIDTH: usize = 30;
const INLINE_LIMIT: usize = 2;
pub const SECTIONS: [&str; 10] = [
"about",
"usage",
"commands",
"args",
"flags",
"grouped_args",
"ungrouped_args",
"grouped_flags",
"ungrouped_flags",
"after_help",
];
pub fn unsupported_section(template: &str) -> Result<Option<&str>, &'static str> {
template::check(template)?;
let mut rest = template;
while let Some(at) = rest.find("{{") {
let after = &rest[at + 2..];
let Some(end) = after.find("}}") else {
return Err("a `{{` with no `}}` after it");
};
let name = after[..end].trim();
if !SECTIONS.contains(&name) {
return Ok(Some(name));
}
rest = &after[end + 2..];
}
Ok(None)
}
#[derive(Default)]
struct Sections {
about: String,
usage: String,
commands: String,
args: String,
flags: String,
grouped_args: String,
ungrouped_args: String,
grouped_flags: String,
ungrouped_flags: String,
flattened: String,
after_help: String,
}
impl Sections {
fn concatenated(&self) -> String {
let mut out = String::new();
for part in [
&self.about,
&self.usage,
&self.commands,
&self.args,
&self.flags,
&self.flattened,
&self.after_help,
] {
out.push_str(part);
}
out
}
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(),
"grouped_args" => self.grouped_args.trim().to_string(),
"ungrouped_args" => self.ungrouped_args.trim().to_string(),
"grouped_flags" => self.grouped_flags.trim().to_string(),
"ungrouped_flags" => self.ungrouped_flags.trim().to_string(),
"after_help" => self.after_help.trim().to_string(),
_ => return None,
})
}
fn substituted(&self, template: &str, style: Style) -> String {
template::substitute(template, style.coloured, |name| self.named(name))
}
}
fn assemble(spec: &Spec<'_>, sections: &Sections, style: Style) -> String {
let page = match spec
.help_template
.filter(|template| !template.trim().is_empty())
{
Some(template) => sections.substituted(template, style),
None => sections.concatenated(),
};
let trimmed = page.trim();
let mut done = String::with_capacity(trimmed.len() + 1);
done.push_str(trimmed);
done.push('\n');
done
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Style {
coloured: bool,
}
impl Style {
pub const PLAIN: Style = Style { coloured: false };
pub const COLOURED: Style = Style { coloured: true };
pub fn auto() -> Style {
use std::io::IsTerminal as _;
Self::auto_for(std::io::stdout().is_terminal())
}
pub fn auto_stderr() -> Style {
use std::io::IsTerminal as _;
Self::auto_for(std::io::stderr().is_terminal())
}
fn auto_for(is_terminal: bool) -> Style {
let forced = std::env::var_os("CLICOLOR_FORCE").is_some_and(|v| v != "0");
let refused = std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty());
if refused {
Style::PLAIN
} else if forced || is_terminal {
Style::COLOURED
} else {
Style::PLAIN
}
}
fn heading(self, text: &str) -> String {
template::semantic("heading", text, self.coloured)
}
fn literal(self, text: &str) -> String {
template::semantic("option", text, self.coloured)
}
fn metavar(self, text: &str) -> String {
template::semantic("metavar", text, self.coloured)
}
fn inline(self, text: &str) -> String {
if !self.coloured {
return text.to_string();
}
styled_inline(text, None)
}
}
fn styled_inline(text: &str, parent: Option<&str>) -> String {
let mut out = String::with_capacity(text.len());
let mut at = 0;
let mut allow_run_remainder = false;
while at < text.len() {
let rest = &text[at..];
if let Some(escaped) = rest
.strip_prefix('\\')
.and_then(|after| after.chars().next())
{
if matches!(escaped, '*' | '_' | '~' | '`' | '\\') {
out.push(escaped);
at += 1 + escaped.len_utf8();
allow_run_remainder = false;
continue;
}
}
let span = [
("***", "1;3", "22;23", false, true),
("___", "1;3", "22;23", true, true),
("**", "1", "22", false, true),
("__", "1", "22", true, true),
("~~", "9", "29", false, true),
("*", "3", "23", false, true),
("_", "3", "23", true, true),
("`", "36", "39", false, false),
]
.into_iter()
.find_map(|(delimiter, open, close, word_boundary, recurse)| {
rest.strip_prefix(delimiter)?;
let marker = delimiter.chars().next().expect("a delimiter has a marker");
let previous = text[..at].chars().next_back();
if (previous == Some(marker) && !allow_run_remainder)
|| (delimiter.len() == 1 && rest[delimiter.len()..].starts_with(marker))
{
return None;
}
if word_boundary && previous.is_some_and(char::is_alphanumeric) {
return None;
}
let content_start = at + delimiter.len();
let (end, after) = closing_delimiter(text, content_start, delimiter, word_boundary, 0)?;
Some((delimiter, open, close, recurse, content_start, end, after))
});
if let Some((delimiter, open, close, recurse, content_start, end, after)) = span {
out.push_str("\u{1b}[");
out.push_str(open);
out.push('m');
if recurse {
out.push_str(&styled_inline(&text[content_start..end], Some(open)));
} else {
out.push_str(&text[content_start..end]);
}
out.push_str("\u{1b}[");
out.push_str(close);
out.push('m');
if let Some(parent) = parent {
out.push_str("\u{1b}[");
out.push_str(parent);
out.push('m');
}
let marker = delimiter.chars().next().expect("a delimiter has a marker");
allow_run_remainder =
text[after..].starts_with(marker) && text[..after].ends_with(marker);
at = after;
continue;
}
let ch = rest.chars().next().expect("at is on a character boundary");
out.push(ch);
at += ch.len_utf8();
allow_run_remainder = false;
}
out
}
fn closing_delimiter(
text: &str,
content_start: usize,
delimiter: &str,
word_boundary: bool,
reserve: usize,
) -> Option<(usize, usize)> {
let marker = delimiter.chars().next()?;
let width = delimiter.len();
let mut search_at = content_start;
while let Some(found) = text[search_at..].find(marker) {
let run_start = search_at + found;
let run_len = text[run_start..]
.chars()
.take_while(|ch| *ch == marker)
.count();
let run_end = run_start + run_len;
let escaped = text[..run_start]
.chars()
.rev()
.take_while(|ch| *ch == '\\')
.count()
% 2
== 1;
if escaped {
search_at = run_start + marker.len_utf8();
continue;
}
let nested_width = match (run_len, marker) {
(1..=3, '*' | '_') if run_len != width => run_len,
_ => 0,
};
if nested_width != 0 {
let nested = &text[run_start..run_start + nested_width];
if let Some((_, after)) =
closing_delimiter(text, run_start + nested_width, nested, marker == '_', width)
{
search_at = after;
continue;
}
}
if run_len >= width {
let after = run_start + width;
let left_in_run = run_end - after;
let leaves_parent_close = left_in_run == 0 || left_in_run >= reserve;
let boundary_ok = !word_boundary
|| !text[after..]
.chars()
.next()
.is_some_and(char::is_alphanumeric);
if run_start > content_start
&& !text[content_start..run_start].trim().is_empty()
&& leaves_parent_close
&& boundary_ok
{
return Some((run_start, after));
}
}
search_at = run_end;
}
None
}
fn styled_flag_usage(usage: &str, style: Style) -> String {
let mut out = String::with_capacity(usage.len());
let mut at = 0;
while at < usage.len() {
let rest = &usage[at..];
let previous = usage[..at].chars().next_back();
if rest.starts_with('-')
&& previous.is_none_or(|c| c.is_whitespace() || matches!(c, ',' | ':' | '[' | '<'))
{
let end = rest
.char_indices()
.skip(1)
.find_map(|(i, c)| {
(c.is_whitespace() || matches!(c, ',' | '=' | '[' | ']' | '<' | '>'))
.then_some(i)
})
.unwrap_or(rest.len());
out.push_str(&style.literal(&rest[..end]));
at += end;
continue;
}
if rest.starts_with("<-") {
out.push('<');
at += 1;
continue;
}
if rest.starts_with('<') {
if let Some(end) = rest.find('>') {
let end = end + 1;
out.push_str(&style.metavar(&rest[..end]));
at += end;
continue;
}
}
if let Some(value) = rest.strip_prefix("[=") {
if let Some(end) = value.find(']') {
out.push_str("[=");
out.push_str(&style.metavar(&value[..end]));
out.push(']');
at += end + 3;
continue;
}
}
if let Some(value) = rest.strip_prefix('=') {
out.push('=');
at += 1;
if !value.starts_with('<') {
let end = value
.find(|c: char| c.is_whitespace() || matches!(c, ',' | ']' | '>'))
.unwrap_or(value.len());
if end > 0 {
out.push_str(&style.metavar(&value[..end]));
at += end;
}
}
continue;
}
if previous == Some('[') && !rest.starts_with('-') {
let end = rest.find(']').unwrap_or(rest.len());
if end > 0 {
out.push_str(&style.metavar(&rest[..end]));
at += end;
continue;
}
}
if rest.starts_with(|c: char| c.is_ascii_uppercase())
&& previous.is_none_or(|c| c.is_whitespace() || matches!(c, '=' | '[' | '<'))
{
let end = rest
.find(|c: char| {
!(c.is_ascii_uppercase() || c.is_ascii_digit() || matches!(c, '_' | '-' | '@'))
})
.unwrap_or(rest.len());
let boundary = rest[end..].chars().next();
if boundary.is_none_or(|c| {
c.is_whitespace() || matches!(c, ',' | '=' | '[' | ']' | '<' | '>' | '.')
}) {
out.push_str(&style.metavar(&rest[..end]));
at += end;
continue;
}
}
let ch = rest.chars().next().expect("at is on a character boundary");
out.push(ch);
at += ch.len_utf8();
}
out
}
fn help_structure(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
inherit_version_actions: bool,
) -> (Vec<String>, Vec<String>, Vec<String>, Vec<String>) {
let meta = *chain.last().expect("a page is always about some command");
let mut headings = Vec::new();
if !page_examples(spec, meta).is_empty() {
headings.push("Examples".to_string());
}
if meta.flatten_help {
flat_help_headings(&path[1.min(path.len())..], meta, &mut headings);
} else if meta.subcommands.iter().any(|sub| !sub.hide) {
headings.push(
meta.subcommand_help_heading
.unwrap_or("Commands")
.to_string(),
);
headings.extend(
meta.subcommands
.iter()
.filter(|sub| !sub.hide)
.filter_map(|sub| sub.help_heading)
.map(str::to_string),
);
}
let (own, inherited) = own_and_global(chain, inherit_version_actions);
let visible_arg = |arg: &&ArgMeta<'_>| {
!arg.hide
&& if long {
!arg.hide_long_help
} else {
!arg.hide_short_help
}
};
let mut args: Vec<_> = meta.args.iter().filter(visible_arg).collect();
order_args(&mut args, meta.args);
if args.iter().any(|arg| arg.help_heading.is_none()) {
headings.push("Arguments".to_string());
}
headings.extend(
args.iter()
.filter_map(|arg| arg.help_heading)
.map(str::to_string),
);
let mut arg_usages: Vec<String> = args.iter().map(|arg| arg_usage(arg)).collect();
let visible_flag = |flag: &&FlagMeta<'_>| {
!flag.hide
&& if long {
!flag.hide_long_help
} else {
!flag.hide_short_help
}
};
let mut own: Vec<_> = own.into_iter().filter(visible_flag).collect();
order_flags(&mut own, meta.flags);
let inherited: Vec<_> = inherited
.into_iter()
.filter(|(flag, _)| {
if long {
!flag.hide_long_help
} else {
!flag.hide_short_help
}
})
.collect();
if own
.iter()
.any(|flag| flag_help_heading(meta, flag).is_none())
{
headings.push("Flags".to_string());
}
headings.extend(
own.iter()
.filter_map(|flag| flag_help_heading(meta, flag))
.map(str::to_string),
);
if !inherited.is_empty() {
headings.push("Global flags".to_string());
}
let mut flag_usages: Vec<String> = own.iter().map(|flag| column_usage(flag)).collect();
flag_usages.extend(inherited.into_iter().map(|(_, usage)| usage));
if meta.flatten_help {
flat_help_usages(meta, long, &mut flag_usages, &mut arg_usages);
}
arg_usages.sort_unstable_by_key(|usage| core::cmp::Reverse(usage.len()));
flag_usages.sort_unstable_by_key(|usage| core::cmp::Reverse(usage.len()));
let mut synopsis = String::new();
usage_section(&mut synopsis, spec, path, meta);
let synopsis = synopsis.lines().map(str::to_string).collect();
(headings, flag_usages, arg_usages, synopsis)
}
fn flat_help_usages(
meta: &CommandMeta<'_>,
long: bool,
flag_usages: &mut Vec<String>,
arg_usages: &mut Vec<String>,
) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
arg_usages.extend(
sub.args
.iter()
.filter(|arg| {
!arg.hide
&& if long {
!arg.hide_long_help
} else {
!arg.hide_short_help
}
})
.map(arg_usage),
);
flag_usages.extend(
sub.flags
.iter()
.filter(|flag| {
!flag.flag.global
&& !flag.hide
&& if long {
!flag.hide_long_help
} else {
!flag.hide_short_help
}
})
.map(column_usage),
);
if sub.flatten_help {
flat_help_usages(sub, long, flag_usages, arg_usages);
}
}
}
fn flat_help_headings(path: &[&str], meta: &CommandMeta<'_>, headings: &mut Vec<String>) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
headings.push(sub_path.join(" "));
if sub.flatten_help {
flat_help_headings(&sub_path, sub, headings);
}
}
}
fn styled_help(
page: &str,
style: Style,
headings: &[String],
flag_usages: &[String],
arg_usages: &[String],
synopsis: &[String],
) -> String {
if !style.coloured {
return page.to_string();
}
let mut out = String::with_capacity(page.len());
for line in page.split_inclusive('\n') {
let (body, newline) = line
.strip_suffix('\n')
.map_or((line, ""), |body| (body, "\n"));
if synopsis.iter().any(|known| known == body) && body.starts_with("Usage:") {
let usage = body.strip_prefix("Usage:").unwrap_or_default();
out.push_str(&style.heading("Usage:"));
out.push_str(&styled_flag_usage(usage, style));
} else if synopsis.iter().any(|known| known == body) {
out.push_str(&styled_flag_usage(body, style));
} else if body
.strip_suffix(':')
.is_some_and(|heading| headings.iter().any(|known| known == heading))
{
out.push_str(&style.heading(body));
} else {
let styled = body.strip_prefix(" ").and_then(|entry| {
flag_usages
.iter()
.find_map(|usage| {
entry
.strip_prefix(usage)
.filter(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
.map(|rest| format!(" {}{rest}", styled_flag_usage(usage, style)))
})
.or_else(|| {
arg_usages.iter().find_map(|usage| {
entry
.strip_prefix(usage)
.filter(|rest| {
rest.is_empty() || rest.starts_with(char::is_whitespace)
})
.map(|rest| format!(" {}{rest}", styled_flag_usage(usage, style)))
})
})
});
let body = styled.as_deref().unwrap_or(body);
if body.trim_start().starts_with("$ ") {
out.push_str(body);
} else {
out.push_str(&style.inline(body));
}
}
out.push_str(newline);
}
out
}
fn assembled_help(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
style: Style,
inherit_version_actions: bool,
) -> String {
let sections = if long {
long_sections(spec, path, chain, inherit_version_actions)
} else {
short_sections(spec, path, chain, inherit_version_actions)
};
let (headings, flag_usages, arg_usages, synopsis) =
help_structure(spec, path, chain, long, inherit_version_actions);
let page = match spec
.help_template
.filter(|template| !template.trim().is_empty())
{
Some(template) => template::substitute(template, style.coloured, |name| {
sections.named(name).map(|part| {
styled_help(
&part,
style,
&headings,
&flag_usages,
&arg_usages,
&synopsis,
)
})
}),
None => styled_help(
§ions.concatenated(),
style,
&headings,
&flag_usages,
&arg_usages,
&synopsis,
),
};
let trimmed = page.trim();
let mut done = String::with_capacity(trimmed.len() + 1);
done.push_str(trimmed);
done.push('\n');
done
}
pub fn usage_line(path: &[&str], meta: &CommandMeta<'_>) -> String {
usage_line_with_subcommands(path, meta, true)
}
fn usage_line_with_subcommands(
path: &[&str],
meta: &CommandMeta<'_>,
include_subcommands: bool,
) -> String {
let mut out = String::new();
for (i, part) in path.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(part);
}
let flags: usize = meta.flags.iter().filter(|f| !f.hide && !f.builtin).count();
if flags > 0 {
let required = meta
.flags
.iter()
.any(|f| !f.hide && !f.builtin && flag_demanded(f));
if flags <= INLINE_LIMIT {
for flag in meta.flags.iter().filter(|f| !f.hide && !f.builtin) {
let (open, close) = if flag_demanded(flag) {
('<', '>')
} else {
('[', ']')
};
let _ = write!(out, " {open}{}{close}", flag_usage(flag));
}
} else if required {
out.push_str(" <FLAGS>");
} else {
out.push_str(" [FLAGS]");
}
}
let args: usize = meta.args.iter().filter(|a| !a.hide).count();
if args > 0 {
let required = meta.args.iter().any(|a| !a.hide && demanded(a));
if args <= INLINE_LIMIT {
for arg in meta.args.iter().filter(|a| !a.hide) {
let _ = write!(out, " {}", arg_usage(arg));
}
} else if required {
out.push_str(" <ARGS>…");
} else {
out.push_str(" [ARGS]…");
}
}
if include_subcommands && !meta.cmd.subcommands.is_empty() {
let name = meta.subcommand_value_name.unwrap_or("SUBCOMMAND");
let _ = write!(out, " <{name}>");
}
out
}
fn usage_section(out: &mut String, spec: &Spec<'_>, path: &[&str], meta: &CommandMeta<'_>) {
if path.len() <= 1 {
if let Some(usage) = spec.usage.filter(|usage| !usage.trim().is_empty()) {
let _ = writeln!(out, "{}", usage.trim());
return;
}
}
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
visible.sort_unstable_by_key(|sub| sub.cmd.name);
if meta.flatten_help && !visible.is_empty() {
let mut lines = Vec::new();
if !meta.subcommand_required || meta.cmd.args_conflicts_with_subcommands {
lines.push(usage_line_with_subcommands(path, meta, false));
}
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
lines.push(usage_line(&sub_path, sub));
}
if let Some((first, rest)) = lines.split_first() {
let _ = writeln!(out, "Usage: {first}");
for line in rest {
let _ = writeln!(out, " {line}");
}
}
} else {
let _ = writeln!(out, "Usage: {}", usage_line(path, meta));
}
}
fn flag_usage(meta: &FlagMeta<'_>) -> String {
flag_usage_masked(meta, &Shown::all(meta))
}
struct Shown<'a> {
long: Option<&'a str>,
short: Option<u8>,
negate: bool,
}
impl<'a> Shown<'a> {
fn all(meta: &'a FlagMeta<'a>) -> Self {
Shown {
long: meta
.flag
.longs
.iter()
.copied()
.find(|long| !meta.hidden_longs.contains(long)),
short: meta
.flag
.shorts
.iter()
.copied()
.find(|short| !meta.hidden_shorts.contains(short)),
negate: meta.flag.negate.is_some(),
}
}
fn surviving(
meta: &'a FlagMeta<'a>,
taken: &[String],
taken_negations: &[String],
every_form: &[String],
) -> Self {
let mine: Vec<String> = meta
.flag
.longs
.iter()
.map(|l| format!("--{l}"))
.chain(meta.flag.shorts.iter().map(|s| format!("-{}", *s as char)))
.collect();
Shown {
long: meta
.flag
.longs
.iter()
.copied()
.find(|l| !meta.hidden_longs.contains(l) && !taken.contains(&format!("--{l}"))),
short: meta.flag.shorts.iter().copied().find(|s| {
!meta.hidden_shorts.contains(s) && !taken.contains(&format!("-{}", *s as char))
}),
negate: meta.flag.negate.is_some_and(|n| {
let spelling = format!("--{n}");
!taken_negations.contains(&spelling)
&& (!every_form.contains(&spelling) || mine.contains(&spelling))
}),
}
}
fn nothing(&self) -> bool {
self.long.is_none() && self.short.is_none() && !self.negate
}
}
fn flag_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
let flag = meta.flag;
let mut out = String::new();
let long = show.long;
let short = show.short.as_ref();
let implied = long.or_else(|| short.map(|_| ""));
let implied_matches = match (implied, short) {
(Some(long), _) if !long.is_empty() => long == flag.name,
(Some(_), Some(short)) => {
let mut buf = [0u8; 4];
(*short as char).encode_utf8(&mut buf) == flag.name
}
_ => show.negate && flag.negate == Some(flag.name),
};
if !implied_matches {
let _ = write!(out, "{}:", flag.name);
}
if let Some(short) = short {
if !out.is_empty() {
out.push(' ');
}
let _ = write!(out, "-{}", *short as char);
}
if let Some(long) = long {
if !out.is_empty() {
out.push(' ');
}
let _ = write!(out, "--{long}");
}
if flag.takes_value {
let exact = exact_arity(meta.value_var_min, meta.value_var_max);
if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
for index in 0..exact.unwrap() {
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
index == 0,
);
}
} else if meta.value_names.len() <= 1 {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
true,
);
} else {
for (index, name) in meta.value_names.iter().enumerate() {
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
index == 0,
);
}
}
if flag.variadic && meta.value_names.len() <= 1 && exact.is_none() {
out.push('…');
}
}
out
}
fn append_flag_value(
out: &mut String,
name: &str,
optional: bool,
require_equals: bool,
first: bool,
) {
if first && optional && require_equals {
let _ = write!(out, "[={name}]");
} else {
let separator = if first && require_equals { "=" } else { " " };
let (open, close) = if optional { ('[', ']') } else { ('<', '>') };
let _ = write!(out, "{separator}{open}{name}{close}");
}
}
fn flag_demanded(meta: &FlagMeta<'_>) -> bool {
meta.required && meta.default.is_empty()
}
fn demanded(meta: &ArgMeta<'_>) -> bool {
meta.required && meta.default.is_empty()
}
pub(crate) fn arg_usage(meta: &ArgMeta<'_>) -> String {
let arg = meta.arg;
let mut out = String::new();
let (open, close) = if demanded(meta) {
('<', '>')
} else {
('[', ']')
};
let exact = exact_arity(meta.var_min, meta.var_max);
if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
for index in 0..exact.unwrap() {
if index > 0 {
out.push(' ');
}
let _ = write!(out, "{open}{}{close}", arg.name);
}
} else if meta.value_names.len() <= 1 {
if arg.double_dash == DoubleDash::Required {
let _ = write!(out, "{open}-- {}{close}", arg.name);
} else {
let _ = write!(out, "{open}{}{close}", arg.name);
}
} else {
if arg.double_dash == DoubleDash::Required {
out.push_str("-- ");
}
for (index, name) in meta.value_names.iter().enumerate() {
if index > 0 {
out.push(' ');
}
let _ = write!(out, "{open}{name}{close}");
}
}
if arg.var && meta.value_names.len() <= 1 && exact.is_none() {
out.push('…');
}
out
}
fn exact_arity(min: Option<usize>, max: Option<usize>) -> Option<usize> {
match (min, max) {
(Some(min), Some(max)) if min == max => Some(min),
_ => None,
}
}
pub fn short_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) -> String {
short_help_with(spec, path, chain, false)
}
fn short_help_with(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> String {
assemble(
spec,
&short_sections(spec, path, chain, inherit_version_actions),
Style::PLAIN,
)
}
fn short_sections(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> Sections {
let meta = *chain.last().expect("a page is always about some command");
let (own, inherited) = own_and_global(chain, inherit_version_actions);
let own: Vec<_> = own
.into_iter()
.filter(|flag| !flag.hide_short_help)
.collect();
let inherited: Vec<_> = inherited
.into_iter()
.filter(|(flag, _)| !flag.hide_short_help)
.collect();
let mut sections = Sections::default();
let width = terminal_width(meta);
let out = &mut sections.about;
if let Some(before) = meta.before_help.or(spec.root.before_help) {
write_wrapped_indented(out, before, width, 0);
out.push('\n');
}
let root = path.len() <= 1;
if root {
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
}
}
let about = if root { spec.about } else { meta.about };
if let Some(about) = about {
write_wrapped_indented(out, about.trim_end(), width, 0);
out.push('\n');
}
command_deprecation(out, meta, 0, width);
usage_section(&mut sections.usage, spec, path, meta);
if !meta.flatten_help {
commands_section(
&mut sections.commands,
&path[1.min(path.len())..],
meta,
width,
false,
);
}
let mut args: Vec<&ArgMeta<'_>> = meta
.args
.iter()
.filter(|a| !a.hide && !a.hide_short_help)
.collect();
order_args(&mut args, meta.args);
let arg_col = args
.iter()
.map(|a| arg_usage(a).chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
split_groups_section(
SectionSink {
page: &mut sections.args,
ungrouped: &mut sections.ungrouped_args,
grouped: &mut sections.grouped_args,
},
"Arguments",
width,
args.iter().copied(),
|a| a.help_heading,
|_| None,
|out, a| {
let usage = arg_usage(a);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = a.help.filter(|h| !h.trim().is_empty()) {
write_wrapped_block(out, help, width);
}
long_annotations(
out,
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
if a.hide_env { &[] } else { a.env_fallback },
if a.hide_env { &[] } else { a.deprecated_env },
if a.hide_default_value { &[] } else { a.default },
AnnotationLayout {
indent: BLOCK_INDENT,
width,
},
);
return;
}
let environment =
inline_environment_notes(a.hide_env, a.env_fallback, a.deprecated_env);
let notes = inline_annotations(
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
environment.as_deref(),
if a.hide_default_value { &[] } else { a.default },
None,
);
entry(
out,
&usage,
with_annotations(a.help, notes).as_deref(),
arg_col,
width,
false,
);
},
);
let flag_col = own
.iter()
.map(|f| column_usage(f).chars().count())
.chain(inherited.iter().map(|(_, u)| u.chars().count()))
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let short_entry = |out: &mut String, f: &FlagMeta<'_>, usage: String| {
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = f.help.filter(|h| !h.trim().is_empty()) {
write_wrapped_block(out, help, width);
}
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_env { &[] } else { f.env_fallback },
if f.hide_env { &[] } else { f.deprecated_env },
if f.hide_default_value { &[] } else { f.default },
AnnotationLayout {
indent: BLOCK_INDENT,
width,
},
);
flag_notes(out, f, BLOCK_INDENT, width);
return;
}
let deprecation =
deprecation_label(f.deprecated, f.deprecated_warn_at, f.deprecated_remove_at);
let environment = inline_environment_notes(f.hide_env, f.env_fallback, f.deprecated_env);
let notes = inline_annotations(
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
environment.as_deref(),
if f.hide_default_value { &[] } else { f.default },
deprecation.as_deref(),
);
entry(
out,
&usage,
with_annotations(f.help, notes).as_deref(),
flag_col,
width,
false,
);
};
split_groups_section(
SectionSink {
page: &mut sections.flags,
ungrouped: &mut sections.ungrouped_flags,
grouped: &mut sections.grouped_flags,
},
"Flags",
width,
own.iter().copied(),
|f| flag_help_heading(meta, f),
|_| None,
|out, f| short_entry(out, f, column_usage(f)),
);
split_groups_section(
SectionSink {
page: &mut sections.flags,
ungrouped: &mut sections.ungrouped_flags,
grouped: &mut sections.grouped_flags,
},
"Global flags",
width,
inherited.iter(),
|_| None,
|_| None,
|out, (f, usage)| short_entry(out, f, usage.clone()),
);
if meta.flatten_help {
flat_commands_short(
&mut sections.flattened,
&path[1.min(path.len())..],
meta,
width,
);
}
examples_section(&mut sections.after_help, spec, meta);
if let Some(after) = meta.after_help.or(spec.root.after_help) {
sections.after_help.push('\n');
write_wrapped_indented(&mut sections.after_help, after, width, 0);
}
sections
}
const HELP_SUBCOMMAND: &str = "help";
const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
fn commands_section(
out: &mut String,
path: &[&str],
meta: &CommandMeta<'_>,
width: usize,
long: bool,
) {
let mut visible: Vec<&&CommandMeta<'_>> = meta.subcommands.iter().filter(|c| !c.hide).collect();
order_commands(&mut visible);
if visible.is_empty() {
return;
}
let mut lines: Vec<(String, &&CommandMeta<'_>)> = visible
.iter()
.map(|sub| {
let mut sub_path: Vec<&str> = path.to_vec();
sub_path.push(sub.cmd.name);
(usage_line(&sub_path, sub), *sub)
})
.collect();
lines.sort_unstable_by(|a, b| {
a.1.display_order
.unwrap_or(999)
.cmp(&b.1.display_order.unwrap_or(999))
.then_with(|| a.0.cmp(&b.0))
});
let show_help = !meta.cmd.disable_help_subcommand;
let col = lines
.iter()
.map(|(_, sub)| sub.cmd.name.chars().count())
.chain(show_help.then(|| HELP_SUBCOMMAND.chars().count()))
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let default_title = meta.subcommand_help_heading.unwrap_or("Commands");
let mut headings = vec![None];
for (_, sub) in &lines {
let heading = command_help_section(sub, default_title);
if !headings.contains(&heading) {
headings.push(heading);
}
}
for heading in headings {
let title = heading.unwrap_or(default_title);
let _ = writeln!(out, "\n{title}:");
if long {
if let Some(prose) = heading.and_then(|title| heading_help(meta, title)) {
write_wrapped_indented(out, prose, width, 2);
out.push('\n');
}
}
for (_, sub) in lines
.iter()
.filter(|(_, sub)| command_help_section(sub, default_title) == heading)
{
entry(
out,
sub.cmd.name,
command_row(sub).as_deref(),
col,
width,
meta.next_line_help,
);
}
if heading.is_none() && show_help {
entry(
out,
HELP_SUBCOMMAND,
Some(HELP_SUBCOMMAND_SUMMARY),
col,
width,
meta.next_line_help,
);
}
}
}
fn command_row<'a>(sub: &'a CommandMeta<'a>) -> Option<Cow<'a, str>> {
let summary = summarize(sub.about)
.or_else(|| summarize(sub.long_about.and_then(|about| about.lines().next())));
let mut visible_aliases = sub
.cmd
.aliases
.iter()
.copied()
.filter(|a| !sub.hidden_aliases.contains(a))
.peekable();
let label = deprecation_label(
sub.deprecated,
sub.deprecated_warn_at,
sub.deprecated_remove_at,
);
if visible_aliases.peek().is_none() && label.is_none() {
return summary.map(Cow::Borrowed);
}
let mut row = String::new();
if let Some(summary) = summary {
row.push_str(summary);
}
if visible_aliases.peek().is_some() {
if !row.is_empty() {
row.push(' ');
}
row.push_str("[aliases: ");
for (index, alias) in visible_aliases.enumerate() {
if index > 0 {
row.push_str(", ");
}
row.push_str(alias);
}
row.push(']');
}
if let Some(label) = label {
if !row.is_empty() {
row.push(' ');
}
row.push_str(&label);
}
Some(Cow::Owned(row))
}
fn flat_commands_short(out: &mut String, path: &[&str], meta: &CommandMeta<'_>, width: usize) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
let _ = writeln!(out, "\n{}:", sub_path.join(" "));
if let Some(about) = sub.about.filter(|about| !about.trim().is_empty()) {
write_wrapped_indented(out, about.trim_end(), width, 0);
}
command_deprecation(out, sub, 0, width);
let mut args: Vec<_> = sub
.args
.iter()
.filter(|arg| !arg.hide && !arg.hide_short_help)
.collect();
order_args(&mut args, sub.args);
let mut flags: Vec<&FlagMeta<'_>> = sub
.flags
.iter()
.filter(|flag| !flag.flag.global && !flag.hide && !flag.hide_short_help)
.collect();
order_flags(&mut flags, sub.flags);
let arg_col = args
.iter()
.map(|arg| arg_usage(arg).chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let flag_col = flags
.iter()
.map(|flag| column_usage(flag).chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
for arg in args {
let usage = arg_usage(arg);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = arg.help.filter(|help| !help.trim().is_empty()) {
write_wrapped_block(out, help, width);
}
long_annotations(
out,
if arg.hide_possible_values {
&[]
} else {
arg.choices
},
if arg.hide_env { None } else { arg.env },
if arg.hide_env { &[] } else { arg.env_fallback },
if arg.hide_env {
&[]
} else {
arg.deprecated_env
},
if arg.hide_default_value {
&[]
} else {
arg.default
},
AnnotationLayout {
indent: BLOCK_INDENT,
width,
},
);
continue;
}
let environment =
inline_environment_notes(arg.hide_env, arg.env_fallback, arg.deprecated_env);
let notes = inline_annotations(
if arg.hide_possible_values {
&[]
} else {
arg.choices
},
if arg.hide_env { None } else { arg.env },
environment.as_deref(),
if arg.hide_default_value {
&[]
} else {
arg.default
},
None,
);
entry(
out,
&usage,
with_annotations(arg.help, notes).as_deref(),
arg_col,
width,
false,
);
}
for flag in flags {
let usage = column_usage(flag);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = flag.help.filter(|help| !help.trim().is_empty()) {
write_wrapped_block(out, help, width);
}
long_annotations(
out,
if flag.hide_possible_values {
&[]
} else {
flag.choices
},
if flag.hide_env { None } else { flag.env },
if flag.hide_env {
&[]
} else {
flag.env_fallback
},
if flag.hide_env {
&[]
} else {
flag.deprecated_env
},
if flag.hide_default_value {
&[]
} else {
flag.default
},
AnnotationLayout {
indent: BLOCK_INDENT,
width,
},
);
flag_notes(out, flag, BLOCK_INDENT, width);
continue;
}
let deprecation = deprecation_label(
flag.deprecated,
flag.deprecated_warn_at,
flag.deprecated_remove_at,
);
let environment =
inline_environment_notes(flag.hide_env, flag.env_fallback, flag.deprecated_env);
let notes = inline_annotations(
if flag.hide_possible_values {
&[]
} else {
flag.choices
},
if flag.hide_env { None } else { flag.env },
environment.as_deref(),
if flag.hide_default_value {
&[]
} else {
flag.default
},
deprecation.as_deref(),
);
entry(
out,
&usage,
with_annotations(flag.help, notes).as_deref(),
flag_col,
width,
false,
);
}
if sub.flatten_help {
flat_commands_short(out, &sub_path, sub, width);
}
out.push('\n');
}
}
fn flag_help_heading<'a>(meta: &'a CommandMeta<'a>, flag: &'a FlagMeta<'a>) -> Option<&'a str> {
flag.help_heading
.or_else(|| flatten_site_heading(meta.flatten_groups, flag))
}
fn flatten_site_heading<'a>(
groups: &'a [crate::spec::FlattenGroup<'a>],
flag: &FlagMeta<'_>,
) -> Option<&'a str> {
for group in groups {
if group
.meta
.flags
.iter()
.any(|candidate| core::ptr::eq(candidate.flag, flag.flag))
{
return group
.help_heading
.or_else(|| flatten_site_heading(group.meta.flatten_groups, flag));
}
if let Some(heading) = flatten_site_heading(group.meta.flatten_groups, flag) {
return Some(heading);
}
}
None
}
fn heading_help<'a>(meta: &'a CommandMeta<'a>, title: &str) -> Option<&'a str> {
if let Some(found) = meta
.headings
.iter()
.find(|heading| heading.title == title)
.map(|heading| heading.help)
{
return Some(found);
}
meta.flatten_groups
.iter()
.find_map(|group| heading_help(group.meta, title))
}
struct SectionSink<'s> {
page: &'s mut String,
ungrouped: &'s mut String,
grouped: &'s mut String,
}
fn split_groups_section<'m, T: 'm>(
sink: SectionSink<'_>,
default_title: &str,
width: usize,
items: impl Iterator<Item = &'m T> + Clone,
heading_of: impl Fn(&T) -> Option<&str>,
prose_of: impl Fn(&str) -> Option<&'m str>,
mut write_item: impl FnMut(&mut String, &T),
) {
let mut headings: Vec<Option<&str>> = Vec::new();
for item in items.clone() {
let heading = heading_of(item);
if !headings.contains(&heading) {
headings.push(heading);
}
}
if let Some(index) = headings.iter().position(Option::is_none) {
let unheaded = headings.remove(index);
headings.insert(0, unheaded);
}
for heading in headings {
let mut section = String::new();
let title = heading.unwrap_or(default_title);
let _ = writeln!(section, "\n{title}:");
if let Some(prose) = heading.and_then(&prose_of) {
write_wrapped_indented(&mut section, prose, width, 2);
section.push('\n');
}
for item in items.clone().filter(|i| heading_of(i) == heading) {
write_item(&mut section, item);
}
sink.page.push_str(§ion);
match heading {
Some(_) => sink.grouped.push_str(§ion),
None => sink.ungrouped.push_str(§ion),
}
}
}
fn order_args<'a>(items: &mut Vec<&'a ArgMeta<'a>>, declared: &'a [ArgMeta<'a>]) {
items.sort_unstable_by_key(|item| {
let position = declared
.iter()
.position(|candidate| core::ptr::eq(candidate, *item))
.unwrap_or(usize::MAX);
(item.display_order.unwrap_or(position), position)
});
}
fn order_flags<'a>(items: &mut Vec<&'a FlagMeta<'a>>, declared: &'a [FlagMeta<'a>]) {
items.sort_unstable_by_key(|item| {
let position = declared
.iter()
.position(|candidate| core::ptr::eq(candidate, *item))
.unwrap_or(usize::MAX);
(item.display_order.unwrap_or(position), position)
});
}
fn order_commands(items: &mut Vec<&&CommandMeta<'_>>) {
items.sort_unstable_by(|a, b| {
a.display_order
.unwrap_or(999)
.cmp(&b.display_order.unwrap_or(999))
.then_with(|| a.cmd.name.cmp(b.cmd.name))
});
}
fn command_help_section<'a>(sub: &'a CommandMeta<'a>, default_title: &str) -> Option<&'a str> {
sub.help_heading.filter(|heading| *heading != default_title)
}
fn inline_annotations(
choices: &[&str],
env: Option<&str>,
environment: Option<&str>,
default: &[&str],
suffix: Option<&str>,
) -> Option<String> {
let mut out = String::new();
let mut push = |part: &str| {
if !out.is_empty() {
out.push(' ');
}
out.push_str(part);
};
if !choices.is_empty() {
push(&format!("[{}]", choices.join(", ")));
}
if let Some(env) = env {
push(&format!("[env: {env}]"));
}
if let Some(environment) = environment {
push(environment);
}
if !default.is_empty() {
push(&format!("(default: {})", default.join(", ")));
}
if let Some(suffix) = suffix {
push(suffix);
}
(!out.is_empty()).then_some(out)
}
fn with_annotations<'a>(
help: Option<&'a str>,
annotations: Option<String>,
) -> Option<Cow<'a, str>> {
match (summarize(help), annotations) {
(Some(help), None) => Some(Cow::Borrowed(help)),
(None, Some(annotations)) => Some(Cow::Owned(annotations)),
(Some(help), Some(annotations)) => Some(Cow::Owned(format!("{help} {annotations}"))),
(None, None) => None,
}
}
#[cfg(feature = "diagnostics")]
pub(crate) fn flag_spelling(meta: &FlagMeta<'_>) -> String {
meta.flag
.longs
.iter()
.find(|long| !meta.hidden_longs.contains(long))
.map(|long| format!("--{long}"))
.or_else(|| {
meta.flag
.shorts
.iter()
.find(|short| !meta.hidden_shorts.contains(short))
.map(|short| format!("-{}", *short as char))
})
.or_else(|| meta.flag.negate.map(|negate| format!("--{negate}")))
.unwrap_or_else(|| meta.flag.name.to_string())
}
fn display_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
let usage = flag_usage_masked(meta, show);
match meta.flag.negate.filter(|_| show.negate) {
Some(negate) if usage.is_empty() => format!("--{negate}"),
Some(negate) if show.long.is_none() && show.short.is_none() => {
format!("{usage} --{negate}")
}
Some(negate) => format!("{usage} / --{negate}"),
None => usage,
}
}
const SHORT_COL: usize = 4;
fn column_usage(meta: &FlagMeta<'_>) -> String {
column_usage_masked(meta, &Shown::all(meta))
}
fn column_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
let rest = display_usage_masked(meta, show);
let Some(long) = show.long else {
return rest;
};
let Some(at) = rest.find(&format!("--{long}")) else {
return rest;
};
let (before, after) = rest.split_at(at);
let short = before.trim();
let bare_short = short.is_empty()
|| (short.starts_with('-') && !short.starts_with("--") && short.chars().count() == 2);
if !bare_short {
return rest;
}
let short = match short {
"" => String::new(),
s => format!("{s},"),
};
format!("{short:<SHORT_COL$}{after}")
}
fn examples_section(out: &mut String, spec: &Spec<'_>, meta: &CommandMeta<'_>) {
let examples = page_examples(spec, meta);
if examples.is_empty() {
return;
}
let _ = writeln!(out, "\nExamples:");
for example in examples {
if let Some(header) = example.header {
let _ = writeln!(out, " {header}:");
}
let _ = writeln!(out, " $ {}", example.code);
}
}
fn page_examples<'a>(spec: &Spec<'a>, meta: &CommandMeta<'a>) -> &'a [Example<'a>] {
if meta.examples.is_empty() {
spec.root.examples
} else {
meta.examples
}
}
fn terminal_width(meta: &CommandMeta<'_>) -> usize {
if let Some(width) = meta.term_width {
return if width == 0 { usize::MAX } else { width };
}
let detected = std::env::var("COLUMNS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(80);
match meta.max_term_width {
Some(0) | None => detected,
Some(max) => detected.min(max),
}
}
fn usage_column_width(longest: usize, terminal_width: usize) -> usize {
if terminal_width == usize::MAX {
return longest;
}
let available = terminal_width.saturating_sub(4);
let cap = available / 5 * 2 + available % 5 * 2 / 5;
longest.min(cap)
}
pub fn long_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) -> String {
long_help_with(spec, path, chain, false)
}
fn long_help_with(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> String {
assemble(
spec,
&long_sections(spec, path, chain, inherit_version_actions),
Style::PLAIN,
)
}
fn long_sections(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> Sections {
let meta = *chain.last().expect("a page is always about some command");
let (own, inherited) = own_and_global(chain, inherit_version_actions);
let own: Vec<_> = own
.into_iter()
.filter(|flag| !flag.hide_long_help)
.collect();
let inherited: Vec<_> = inherited
.into_iter()
.filter(|(flag, _)| !flag.hide_long_help)
.collect();
let width = terminal_width(meta);
let mut sections = Sections::default();
let out = &mut sections.about;
if let Some(before) = meta
.before_long_help
.or(meta.before_help)
.or(spec.root.before_long_help)
.or(spec.root.before_help)
{
write_wrapped_indented(out, before, width, 0);
out.push('\n');
}
let root = path.len() <= 1;
if root {
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
}
}
let about = if root {
spec.long_about.or(spec.about)
} else {
meta.long_about.or(meta.about)
};
if let Some(about) = about {
write_wrapped_indented(out, about.trim_end(), width, 0);
out.push('\n');
}
command_deprecation(out, meta, 0, width);
usage_section(&mut sections.usage, spec, path, meta);
if !meta.flatten_help {
commands_section(
&mut sections.commands,
&path[1.min(path.len())..],
meta,
width,
true,
);
}
let mut args: Vec<&ArgMeta<'_>> = meta
.args
.iter()
.filter(|a| !a.hide && !a.hide_long_help)
.collect();
order_args(&mut args, meta.args);
let arg_col = args
.iter()
.map(|a| arg_usage(a).chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
split_groups_section(
SectionSink {
page: &mut sections.args,
ungrouped: &mut sections.ungrouped_args,
grouped: &mut sections.grouped_args,
},
"Arguments",
width,
args.iter().copied(),
|a| a.help_heading,
|title| heading_help(meta, title),
|out, a| {
let text = a.long_help.or(a.help);
let indent = entry(
out,
&arg_usage(a),
text,
arg_col,
width,
meta.next_line_help,
);
admonitions(out, a.admonitions, width);
long_annotations(
out,
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
if a.hide_env { &[] } else { a.env_fallback },
if a.hide_env { &[] } else { a.deprecated_env },
if a.hide_default_value { &[] } else { a.default },
AnnotationLayout { indent, width },
);
},
);
let flag_col = own
.iter()
.map(|f| column_usage(f).chars().count())
.chain(inherited.iter().map(|(_, u)| u.chars().count()))
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
split_groups_section(
SectionSink {
page: &mut sections.flags,
ungrouped: &mut sections.ungrouped_flags,
grouped: &mut sections.grouped_flags,
},
"Flags",
width,
own.iter().copied(),
|f| flag_help_heading(meta, f),
|title| heading_help(meta, title),
|out, f| {
let text = f.long_help.or(f.help);
let indent = entry(
out,
&column_usage(f),
text,
flag_col,
width,
meta.next_line_help,
);
admonitions(out, f.admonitions, width);
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_env { &[] } else { f.env_fallback },
if f.hide_env { &[] } else { f.deprecated_env },
if f.hide_default_value { &[] } else { f.default },
AnnotationLayout { indent, width },
);
flag_notes(out, f, indent, width);
},
);
split_groups_section(
SectionSink {
page: &mut sections.flags,
ungrouped: &mut sections.ungrouped_flags,
grouped: &mut sections.grouped_flags,
},
"Global flags",
width,
inherited.iter(),
|_| None,
|_| None,
|out, (f, usage)| {
let text = f.long_help.or(f.help);
let indent = entry(out, usage, text, flag_col, width, meta.next_line_help);
admonitions(out, f.admonitions, width);
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_env { &[] } else { f.env_fallback },
if f.hide_env { &[] } else { f.deprecated_env },
if f.hide_default_value { &[] } else { f.default },
AnnotationLayout { indent, width },
);
flag_notes(out, f, indent, width);
},
);
if meta.flatten_help {
flat_commands_long(
&mut sections.flattened,
&path[1.min(path.len())..],
meta,
width,
);
}
let out = &mut sections.after_help;
let examples = page_examples(spec, meta);
if !examples.is_empty() {
let _ = writeln!(out, "\nExamples:");
for example in examples {
if let Some(header) = example.header {
let _ = writeln!(out, " {header}:");
}
if let Some(help) = example.help {
let _ = writeln!(out, " {help}");
}
let _ = writeln!(out, " $ {}", example.code);
}
}
let after = meta
.after_long_help
.or(meta.after_help)
.or(spec.root.after_long_help)
.or(spec.root.after_help);
if let Some(after) = after {
out.push('\n');
write_wrapped_indented(out, after, width, 0);
}
if spec.author.is_some() || spec.license.is_some() {
out.push('\n');
if let Some(author) = spec.author {
let _ = writeln!(out, "Author: {author}");
}
if let Some(license) = spec.license {
let _ = writeln!(out, "License: {license}");
}
}
sections
}
fn write_indented(out: &mut String, text: &str, indent: usize) {
let pad = " ".repeat(indent);
for (i, line) in text.lines().enumerate() {
if i == 0 || !line.is_empty() {
let _ = writeln!(out, "{pad}{line}");
} else {
out.push('\n');
}
}
if text.ends_with('\n') {
out.push('\n');
}
}
fn entry(
out: &mut String,
usage: &str,
help: Option<&str>,
col: usize,
width: usize,
next_line: bool,
) -> usize {
let indent = 2 + col + 2;
let room = width.saturating_sub(indent);
let overflow = usage.chars().count() > col;
let inline_start = if overflow {
2 + usage.chars().count() + 2
} else {
indent
};
let inline_room = width.saturating_sub(inline_start);
let can_inline = !next_line
&& if overflow {
inline_room >= MIN_INLINE_HELP_WIDTH
} else {
room >= 10
};
let block = !can_inline;
let block_indent = if !next_line && width.saturating_sub(indent) >= 10 {
indent
} else {
BLOCK_INDENT
};
let Some(help) = help.filter(|h| !h.trim().is_empty()) else {
let _ = writeln!(out, " {usage}");
return if block { block_indent } else { indent };
};
if block {
let _ = writeln!(out, " {usage}");
write_wrapped_indented(out, help, width, block_indent);
return block_indent;
}
if overflow {
let lines = wrap_at(help, inline_room, room);
let _ = writeln!(out, " {usage} {}", lines[0]);
for line in &lines[1..] {
if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, "{}{line}", " ".repeat(indent));
}
}
return indent;
}
if fits(help, room) {
out.push_str(" ");
out.push_str(usage);
for _ in 0..col.saturating_sub(usage.chars().count()) {
out.push(' ');
}
out.push_str(" ");
out.push_str(help);
out.push('\n');
return indent;
}
let lines = wrap(help, room);
let _ = writeln!(out, " {usage:<col$} {}", lines[0]);
for line in &lines[1..] {
if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, "{}{line}", " ".repeat(indent));
}
}
indent
}
fn wrap_at(text: &str, first_width: usize, continuation_width: usize) -> Vec<String> {
let mut lines = Vec::new();
for (index, line) in text.split('\n').enumerate() {
lines.extend(wrap(
line,
if index == 0 {
first_width
} else {
continuation_width
},
));
}
lines
}
fn write_wrapped_block(out: &mut String, help: &str, width: usize) {
write_wrapped_indented(out, help, width, BLOCK_INDENT);
}
fn write_wrapped_indented(out: &mut String, help: &str, width: usize, indent: usize) {
let room = width.saturating_sub(indent);
let pad = " ".repeat(indent);
for line in wrap(help, room) {
if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, "{pad}{line}");
}
}
}
fn fits(text: &str, room: usize) -> bool {
if text.len() > room {
return false;
}
let mut after_space = true;
for &byte in text.as_bytes() {
match byte {
b' ' if after_space => return false,
b' ' => after_space = true,
b'\t' | b'\n' | b'\r' | 0x0b | 0x0c => return false,
0x80.. => return false,
_ => after_space = false,
}
}
!after_space
}
fn wrap(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
for paragraph in text.split('\n') {
if paragraph.is_empty() {
lines.push(String::new());
continue;
}
if paragraph.starts_with(" ") || paragraph.starts_with('\t') {
lines.push(paragraph.to_string());
continue;
}
let trimmed = paragraph.trim();
let leading_trimmed = paragraph.trim_start_matches(' ');
let (prefix, body) = match list_prefix(leading_trimmed) {
Some((marker, body)) => (
¶graph[..paragraph.len() - leading_trimmed.len() + marker.len()],
body,
),
None => ("", trimmed),
};
let body_width = width.saturating_sub(prefix.chars().count());
let mut line_prefix = prefix.to_string();
let mut line = String::new();
let mut line_width = 0;
for word in body.split_whitespace() {
let word_width = word.chars().count();
if !line.is_empty() && line_width + 1 + word_width > body_width {
lines.push(format!("{line_prefix}{line}"));
line.clear();
line_prefix = " ".repeat(prefix.chars().count());
line_width = 0;
}
if !line.is_empty() {
line.push(' ');
line_width += 1;
}
line.push_str(word);
line_width += word_width;
}
if !line.is_empty() {
lines.push(format!("{line_prefix}{line}"));
}
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
fn list_prefix(line: &str) -> Option<(&str, &str)> {
for marker in ["* ", "- ", "+ "] {
if let Some(body) = line.strip_prefix(marker) {
return Some((marker, body));
}
}
let digits = line.bytes().take_while(u8::is_ascii_digit).count();
if digits > 0 && line.as_bytes().get(digits..digits + 2) == Some(b". ") {
return Some(line.split_at(digits + 2));
}
None
}
fn long_annotations(
out: &mut String,
choices: &[&str],
env: Option<&str>,
env_fallback: &[&str],
deprecated_env: &[&str],
default: &[&str],
layout: AnnotationLayout,
) {
let AnnotationLayout { indent, width } = layout;
if choices.is_empty()
&& env.is_none()
&& env_fallback.is_empty()
&& deprecated_env.is_empty()
&& default.is_empty()
{
return;
}
if !choices.is_empty() {
write_wrapped_indented(
out,
&format!("[possible values: {}]", choices.join(", ")),
width,
indent,
);
}
if let Some(env) = env {
write_wrapped_indented(out, &format!("[env: {env}]"), width, indent);
}
for env in env_fallback {
write_wrapped_indented(out, &format!("[env fallback: {env}]"), width, indent);
}
for env in deprecated_env {
write_wrapped_indented(out, &format!("[deprecated env: {env}]"), width, indent);
}
if !default.is_empty() {
write_wrapped_indented(
out,
&format!("(default: {})", default.join(", ")),
width,
indent,
);
}
}
#[derive(Clone, Copy)]
struct AnnotationLayout {
indent: usize,
width: usize,
}
fn admonitions(out: &mut String, blocks: &[AdmonitionMeta<'_>], width: usize) {
for block in blocks {
let label = match block.kind {
AdmonitionKind::Note => "Note",
AdmonitionKind::Warning => "Warning",
};
write_labelled(out, label, block.text, width, 4);
}
}
fn write_labelled(out: &mut String, label: &str, text: &str, width: usize, indent: usize) {
let prefix = format!("{label}: ");
let continuation = indent + prefix.chars().count();
let lines = wrap(text, width.saturating_sub(continuation));
let pad = " ".repeat(indent);
let continuation_pad = " ".repeat(continuation);
for (index, line) in lines.iter().enumerate() {
if index == 0 {
let _ = writeln!(out, "{pad}{prefix}{line}");
} else if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, "{continuation_pad}{line}");
}
}
}
fn summarize(text: Option<&str>) -> Option<&str> {
text.map(str::trim_end).filter(|text| !text.is_empty())
}
fn deprecation_label(
message: Option<&str>,
warn_at: Option<&str>,
remove_at: Option<&str>,
) -> Option<String> {
if message.is_none() && warn_at.is_none() && remove_at.is_none() {
return None;
}
let mut parts = Vec::new();
if let Some(message) = message {
parts.push(message.to_string());
}
if let Some(at) = warn_at {
parts.push(format!("warns at {at}"));
}
if let Some(at) = remove_at {
parts.push(format!("removed at {at}"));
}
Some(format!("[deprecated: {}]", parts.join("; ")))
}
fn command_deprecation(out: &mut String, meta: &CommandMeta<'_>, indent: usize, width: usize) {
if let Some(label) = deprecation_label(
meta.deprecated,
meta.deprecated_warn_at,
meta.deprecated_remove_at,
) {
write_wrapped_indented(out, &label, width, indent);
}
}
fn inline_environment_notes(hide: bool, fallbacks: &[&str], deprecated: &[&str]) -> Option<String> {
let mut notes = Vec::new();
if !hide {
notes.extend(fallbacks.iter().map(|env| format!("[env fallback: {env}]")));
notes.extend(
deprecated
.iter()
.map(|env| format!("[deprecated env: {env}]")),
);
}
(!notes.is_empty()).then(|| notes.join(" "))
}
fn flag_notes(out: &mut String, meta: &FlagMeta<'_>, indent: usize, width: usize) {
if let Some(label) = deprecation_label(
meta.deprecated,
meta.deprecated_warn_at,
meta.deprecated_remove_at,
) {
write_wrapped_indented(out, &label, width, indent);
}
}
fn flat_commands_long(out: &mut String, path: &[&str], meta: &CommandMeta<'_>, width: usize) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
let _ = writeln!(out, "\n{}:", sub_path.join(" "));
if let Some(about) = sub
.long_about
.or(sub.about)
.filter(|about| !about.trim().is_empty())
{
write_wrapped_indented(out, about.trim_end(), width, 0);
}
command_deprecation(out, sub, 0, width);
let mut args: Vec<_> = sub
.args
.iter()
.filter(|arg| !arg.hide && !arg.hide_long_help)
.collect();
order_args(&mut args, sub.args);
let mut flags: Vec<&FlagMeta<'_>> = sub
.flags
.iter()
.filter(|flag| !flag.flag.global && !flag.hide && !flag.hide_long_help)
.collect();
order_flags(&mut flags, sub.flags);
let arg_col = args
.iter()
.map(|arg| arg_usage(arg).chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let flag_col = flags
.iter()
.map(|flag| column_usage(flag).chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
for arg in args {
entry(
out,
&arg_usage(arg),
arg.long_help.or(arg.help),
arg_col,
width,
meta.next_line_help,
);
admonitions(out, arg.admonitions, width);
long_annotations(
out,
if arg.hide_possible_values {
&[]
} else {
arg.choices
},
if arg.hide_env { None } else { arg.env },
if arg.hide_env { &[] } else { arg.env_fallback },
if arg.hide_env {
&[]
} else {
arg.deprecated_env
},
if arg.hide_default_value {
&[]
} else {
arg.default
},
AnnotationLayout {
indent: BLOCK_INDENT,
width,
},
);
}
for flag in flags {
entry(
out,
&column_usage(flag),
flag.long_help.or(flag.help),
flag_col,
width,
meta.next_line_help,
);
admonitions(out, flag.admonitions, width);
long_annotations(
out,
if flag.hide_possible_values {
&[]
} else {
flag.choices
},
if flag.hide_env { None } else { flag.env },
if flag.hide_env {
&[]
} else {
flag.env_fallback
},
if flag.hide_env {
&[]
} else {
flag.deprecated_env
},
if flag.hide_default_value {
&[]
} else {
flag.default
},
AnnotationLayout {
indent: BLOCK_INDENT,
width,
},
);
flag_notes(out, flag, BLOCK_INDENT, width);
}
if sub.flatten_help {
flat_commands_long(out, &sub_path, sub, width);
}
out.push('\n');
}
}
pub fn find<'a>(
spec: &Spec<'a>,
cmd: &Command<'_>,
) -> Option<(Vec<&'a str>, Vec<&'a CommandMeta<'a>>)> {
fn walk<'a>(
path: &mut Vec<&'a str>,
chain: &mut Vec<&'a CommandMeta<'a>>,
meta: &'a CommandMeta<'a>,
cmd: &Command<'_>,
) -> bool {
chain.push(meta);
if core::ptr::eq(meta.cmd, cmd) {
return true;
}
for sub in meta.subcommands {
path.push(sub.cmd.name);
if walk(path, chain, sub, cmd) {
return true;
}
path.pop();
}
chain.pop();
false
}
let mut path = vec![spec.bin.unwrap_or(spec.name)];
let mut chain = Vec::new();
walk(&mut path, &mut chain, spec.root, cmd).then_some((path, chain))
}
mod supplied {
use crate::spec::FlagMeta;
use crate::{ArgAction, Flag};
macro_rules! entry {
($name:ident, $flag:ident, $key:expr, $label:expr, $longs:expr, $shorts:expr, $help:expr, $action:expr) => {
static $flag: Flag<'static> = Flag {
key: $key,
name: $label,
longs: $longs,
shorts: $shorts,
action: $action,
..Flag::BOOL
};
pub static $name: FlagMeta<'static> = FlagMeta {
flag: &$flag,
help: Some($help),
builtin: true,
..FlagMeta::EMPTY
};
};
}
entry!(
HELP_BOTH,
HB,
crate::HELP_LONG_KEY,
"help",
&["help"],
b"h",
"Print help",
ArgAction::Help
);
entry!(
HELP_LONG_ONLY,
HL,
crate::HELP_LONG_KEY,
"help",
&["help"],
b"",
"Print help",
ArgAction::Help
);
entry!(
HELP_SHORT_ONLY,
HS,
crate::HELP_SHORT_KEY,
"h",
&[],
b"h",
"Print help",
ArgAction::Help
);
entry!(
VERSION_BOTH,
VB,
crate::VERSION_LONG_KEY,
"version",
&["version"],
b"V",
"Print version",
ArgAction::Version
);
entry!(
VERSION_LONG_ONLY,
VL,
crate::VERSION_LONG_KEY,
"version",
&["version"],
b"",
"Print version",
ArgAction::Version
);
entry!(
VERSION_SHORT_ONLY,
VS,
crate::VERSION_SHORT_KEY,
"V",
&[],
b"V",
"Print version",
ArgAction::Version
);
}
pub(crate) fn supplied_entries(
cmd: &Command<'_>,
taken: &[String],
) -> Vec<&'static FlagMeta<'static>> {
let pick = |long: &str, short: char, both, l, s| match (
taken.contains(&format!("--{long}")),
taken.contains(&format!("-{short}")),
) {
(true, true) => None,
(true, false) => Some(s),
(false, true) => Some(l),
(false, false) => Some(both),
};
let mut out = Vec::new();
if !cmd.disable_help_flag {
out.extend(pick(
"help",
'h',
&supplied::HELP_BOTH,
&supplied::HELP_LONG_ONLY,
&supplied::HELP_SHORT_ONLY,
));
}
if cmd.version && !cmd.disable_version_flag {
out.extend(pick(
"version",
'V',
&supplied::VERSION_BOTH,
&supplied::VERSION_LONG_ONLY,
&supplied::VERSION_SHORT_ONLY,
));
}
out
}
fn own_and_global<'a>(
chain: &[&'a CommandMeta<'a>],
inherit_version_actions: bool,
) -> (Vec<&'a FlagMeta<'a>>, Vec<(&'a FlagMeta<'a>, String)>) {
let Some((here, ancestors)) = chain.split_last() else {
return (Vec::new(), Vec::new());
};
let own: Vec<&FlagMeta<'_>> = here.flags.iter().filter(|f| !f.hide).collect();
fn forms<'f>(f: &'f FlagMeta<'_>) -> impl Iterator<Item = String> + 'f {
f.flag
.longs
.iter()
.map(|l| format!("--{l}"))
.chain(f.flag.shorts.iter().map(|s| format!("-{}", *s as char)))
}
fn negation(f: &FlagMeta<'_>) -> Option<String> {
f.flag.negate.map(|n| format!("--{n}"))
}
let every_form: Vec<String> = here
.flags
.iter()
.chain(ancestors.iter().flat_map(|m| m.flags.iter()).filter(|f| {
f.flag.global || (inherit_version_actions && crate::is_version_flag(f.flag))
}))
.flat_map(forms)
.collect();
let mut taken: Vec<String> = here.flags.iter().flat_map(forms).collect();
let mut taken_negations: Vec<String> = here.flags.iter().filter_map(negation).collect();
let mut keep: Vec<(*const FlagMeta<'_>, Shown<'_>)> = Vec::new();
for meta in ancestors.iter().rev() {
for f in meta.flags.iter().filter(|f| {
f.flag.global || (inherit_version_actions && crate::is_version_flag(f.flag))
}) {
let show = Shown::surviving(f, &taken, &taken_negations, &every_form);
taken.extend(forms(f));
taken_negations.extend(negation(f));
if f.hide || show.nothing() {
continue;
}
keep.push((f as *const _, show));
}
}
let mut inherited: Vec<(&FlagMeta<'_>, String)> = ancestors
.iter()
.flat_map(|meta| meta.flags.iter())
.filter_map(|f| {
keep.iter()
.find(|(p, _)| core::ptr::eq(*p, f as *const _))
.map(|(_, show)| (f, column_usage_masked(f, show)))
})
.collect();
let inherited_positions: Vec<*const FlagMeta<'_>> = inherited
.iter()
.map(|(flag, _)| *flag as *const _)
.collect();
inherited.sort_unstable_by_key(|(flag, _)| {
let position = inherited_positions
.iter()
.position(|candidate| core::ptr::eq(*candidate, *flag as *const _))
.unwrap_or(usize::MAX);
(flag.display_order.unwrap_or(position), position)
});
let mut own = own;
order_flags(&mut own, here.flags);
let claimed: Vec<String> = taken
.iter()
.cloned()
.chain(taken_negations.iter().cloned())
.collect();
if inherit_version_actions {
if let Some(root) = ancestors.first() {
inherited.extend(
supplied_entries(root.cmd, &claimed)
.into_iter()
.filter(|flag| {
matches!(
flag.flag.key,
crate::VERSION_LONG_KEY | crate::VERSION_SHORT_KEY
)
})
.map(|flag| (flag, column_usage(flag))),
);
}
}
own.extend(supplied_entries(here.cmd, &claimed));
(own, inherited)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Topic {
pub id: String,
pub title: String,
}
fn topic_id(title: &str) -> String {
let mut out = String::new();
let mut separator = false;
for ch in title.chars() {
if ch.is_alphanumeric() {
if separator && !out.is_empty() {
out.push('-');
}
out.extend(ch.to_lowercase());
separator = false;
} else {
separator = true;
}
}
if out.is_empty() {
"topic".to_string()
} else {
out
}
}
fn topic_blocks<'m>(
sections: &Sections,
prose_of: impl Fn(&str) -> Option<&'m str>,
) -> Vec<(String, String)> {
let mut topics: Vec<(String, String)> = Vec::new();
for section in [§ions.commands, §ions.args, §ions.flags] {
let mut title: Option<&str> = None;
let mut block = String::new();
let finish =
|title: Option<&str>, block: &mut String, topics: &mut Vec<(String, String)>| {
let Some(title) = title else {
block.clear();
return;
};
let text = block.trim().to_string();
let Some((_, body)) = text.split_once('\n') else {
block.clear();
return;
};
if body.trim().is_empty() {
block.clear();
return;
}
if let Some((_, existing)) = topics.iter_mut().find(|(known, _)| known == title) {
let mut body = body;
if let Some(prose) = prose_of(title) {
let mut introduction = String::new();
write_indented(&mut introduction, prose, 2);
if let Some(rest) = body.strip_prefix(introduction.trim_end_matches('\n')) {
body = rest.trim_start_matches('\n');
}
}
if !existing.is_empty() {
existing.push_str("\n\n");
}
existing.push_str(body);
} else {
topics.push((title.to_string(), text));
}
block.clear();
};
for line in section.lines() {
let heading = (!line.starts_with(char::is_whitespace))
.then(|| line.strip_suffix(':'))
.flatten();
if let Some(heading) = heading {
finish(title, &mut block, &mut topics);
title = Some(heading);
}
if title.is_some() {
if !block.is_empty() {
block.push('\n');
}
block.push_str(line);
}
}
finish(title, &mut block, &mut topics);
}
topics
}
fn topics_with_blocks(
spec: &Spec<'_>,
cmd: &Command<'_>,
long: bool,
) -> Option<Vec<(Topic, String)>> {
let (path, chain) = find(spec, cmd)?;
let sections = if long {
long_sections(spec, &path, &chain, false)
} else {
short_sections(spec, &path, &chain, false)
};
let mut used = Vec::<String>::new();
Some(
topic_blocks(§ions, |title| heading_help(chain.last()?, title))
.into_iter()
.map(|(title, block)| {
let base = topic_id(&title);
let mut id = base.clone();
let mut suffix = 2;
while used.contains(&id) {
id = format!("{base}-{suffix}");
suffix += 1;
}
used.push(id.clone());
(Topic { id, title }, block)
})
.collect(),
)
}
pub fn topics(spec: &Spec<'_>, cmd: &Command<'_>, long: bool) -> Option<Vec<Topic>> {
Some(
topics_with_blocks(spec, cmd, long)?
.into_iter()
.map(|(topic, _)| topic)
.collect(),
)
}
pub fn render_topic(spec: &Spec<'_>, cmd: &Command<'_>, topic: &str, long: bool) -> Option<String> {
topics_with_blocks(spec, cmd, long)?
.into_iter()
.find(|(known, _)| known.id == topic || known.title.eq_ignore_ascii_case(topic))
.map(|(_, mut block)| {
block.push('\n');
block
})
}
pub fn render(spec: &Spec<'_>, cmd: &Command<'_>, long: bool) -> Option<String> {
let (path, chain) = find(spec, cmd)?;
Some(if long {
long_help(spec, &path, &chain)
} else {
short_help(spec, &path, &chain)
})
}
pub fn render_styled(
spec: &Spec<'_>,
cmd: &Command<'_>,
long: bool,
style: Style,
) -> Option<String> {
let (path, chain) = find(spec, cmd)?;
Some(assembled_help(spec, &path, &chain, long, style, false))
}
pub fn render_all(spec: &Spec<'_>, cmd: &Command<'_>) -> Option<String> {
render_all_styled(spec, cmd, Style::PLAIN)
}
pub fn render_all_styled(spec: &Spec<'_>, cmd: &Command<'_>, style: Style) -> Option<String> {
let (path, chain) = find(spec, cmd)?;
Some(recursive_help(spec, path, chain, style, false))
}
pub fn route_to<'t>(
root: &'t Command<'t>,
argv: &[&std::ffi::OsStr],
cmd: &Command<'_>,
) -> Option<Vec<&'t Command<'t>>> {
let mut parser = crate::Parser::new(root, argv);
while let Some(event) = parser.next_event() {
if event.is_err() {
break;
}
}
let (help_from, help_to) = parser.help_span();
let mut route: Vec<&Command<'_>> = parser.command_path().into_iter().map(|(c, _)| c).collect();
if route.is_empty() {
route.push(root);
}
for token in argv.get(help_from..help_to).unwrap_or_default() {
let here = *route.last()?;
let word = token.as_encoded_bytes();
let next = crate::find_named(here, word)?;
route.push(next);
}
core::ptr::eq(*route.last()?, cmd).then_some(route)
}
pub fn route_to_view<'t>(
root: &'t Command<'t>,
argv: &[&std::ffi::OsStr],
cmd: &Command<'_>,
view: &ViewMeta<'_>,
) -> Option<Vec<&'t Command<'t>>> {
let words = argv.get(1..).unwrap_or_default();
let mut rewritten =
Vec::with_capacity(words.len() + view.root.split_ascii_whitespace().count());
rewritten.extend(view.root.split_ascii_whitespace().map(std::ffi::OsStr::new));
rewritten.extend_from_slice(words);
route_to(root, &rewritten, cmd)
}
fn route_context<'a>(
spec: &'a Spec<'a>,
route: &[&Command<'_>],
) -> Option<(Vec<&'a str>, Vec<&'a CommandMeta<'a>>)> {
let mut names = vec![spec.bin.unwrap_or(spec.name)];
let mut chain = vec![spec.root];
for cmd in route.iter().skip(1) {
let here = chain.last()?;
let next = here
.subcommands
.iter()
.find(|sub| core::ptr::eq(sub.cmd, *cmd))?;
names.push(next.cmd.name);
chain.push(next);
}
Some((names, chain))
}
pub fn render_at(spec: &Spec<'_>, route: &[&Command<'_>], long: bool) -> Option<String> {
let (names, chain) = route_context(spec, route)?;
Some(if long {
long_help(spec, &names, &chain)
} else {
short_help(spec, &names, &chain)
})
}
pub fn render_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
long: bool,
style: Style,
) -> Option<String> {
let (path, chain) = route_context(spec, route)?;
Some(assembled_help(spec, &path, &chain, long, style, false))
}
pub fn render_view_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
view: &ViewMeta<'_>,
long: bool,
style: Style,
) -> Option<String> {
let (canonical_path, canonical_chain) = route_context(spec, route)?;
let depth = view.root.split_ascii_whitespace().count();
let promoted = *canonical_chain.get(depth)?;
let (root_flags, root_groups) = view_root_fields(spec, promoted, view);
let root_command = Command {
version: spec.root.cmd.version,
disable_version_flag: spec.root.cmd.disable_version_flag,
..*promoted.cmd
};
let root = CommandMeta {
cmd: &root_command,
flags: &root_flags,
groups: &root_groups,
..*promoted
};
let mut chain = Vec::with_capacity(canonical_chain.len());
chain.push(&root);
chain.extend_from_slice(canonical_chain.get(depth + 1..).unwrap_or_default());
let mut path = Vec::with_capacity(canonical_path.len().saturating_sub(depth));
path.push(view.bin);
path.extend_from_slice(canonical_path.get(depth + 1..).unwrap_or_default());
let viewed = Spec {
name: view.name,
bin: Some(view.bin),
about: promoted.about,
long_about: promoted.long_about,
usage: None,
default_subcommand: None,
multicall: false,
root: &root,
..*spec
};
Some(assembled_help(&viewed, &path, &chain, long, style, true))
}
pub fn render_all_at(spec: &Spec<'_>, route: &[&Command<'_>]) -> Option<String> {
render_all_at_styled(spec, route, Style::PLAIN)
}
pub fn render_all_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
style: Style,
) -> Option<String> {
let (path, chain) = route_context(spec, route)?;
Some(recursive_help(spec, path, chain, style, false))
}
pub fn render_all_view_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
view: &ViewMeta<'_>,
style: Style,
) -> Option<String> {
let (canonical_path, canonical_chain) = route_context(spec, route)?;
let depth = view.root.split_ascii_whitespace().count();
let promoted = *canonical_chain.get(depth)?;
let (root_flags, root_groups) = view_root_fields(spec, promoted, view);
let root_command = Command {
version: spec.root.cmd.version,
disable_version_flag: spec.root.cmd.disable_version_flag,
..*promoted.cmd
};
let root = CommandMeta {
cmd: &root_command,
flags: &root_flags,
groups: &root_groups,
..*promoted
};
let mut chain = Vec::with_capacity(canonical_chain.len().saturating_sub(depth));
chain.push(&root);
chain.extend_from_slice(canonical_chain.get(depth + 1..).unwrap_or_default());
let mut path = Vec::with_capacity(canonical_path.len().saturating_sub(depth));
path.push(view.bin);
path.extend_from_slice(canonical_path.get(depth + 1..).unwrap_or_default());
let viewed = Spec {
name: view.name,
bin: Some(view.bin),
about: promoted.about,
long_about: promoted.long_about,
usage: None,
default_subcommand: None,
multicall: false,
root: &root,
..*spec
};
Some(recursive_help(&viewed, path, chain, style, true))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
Short,
Long,
All,
}
pub fn page(
spec: &Spec<'_>,
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
cmd: &Command<'_>,
page: Page,
style: Style,
) -> Option<String> {
match route_to(root, argv, cmd) {
Some(route) => match page {
Page::Short => render_at_styled(spec, &route, false, style),
Page::Long => render_at_styled(spec, &route, true, style),
Page::All => render_all_at_styled(spec, &route, style),
},
None => match page {
Page::Short => render_styled(spec, cmd, false, style),
Page::Long => render_styled(spec, cmd, true, style),
Page::All => render_all_styled(spec, cmd, style),
},
}
}
pub fn page_view(
spec: &Spec<'_>,
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
cmd: &Command<'_>,
view: &ViewMeta<'_>,
page: Page,
style: Style,
) -> Option<String> {
match route_to_view(root, argv, cmd, view) {
Some(route) => match page {
Page::Short => render_view_at_styled(spec, &route, view, false, style),
Page::Long => render_view_at_styled(spec, &route, view, true, style),
Page::All => render_all_view_at_styled(spec, &route, view, style),
},
None => match page {
Page::Short => render_styled(spec, cmd, false, style),
Page::Long => render_styled(spec, cmd, true, style),
Page::All => render_all_styled(spec, cmd, style),
},
}
}
pub(crate) fn view_root_flags<'a>(
spec: &'a Spec<'a>,
promoted: &CommandMeta<'a>,
view: &ViewMeta<'a>,
) -> Vec<FlagMeta<'a>> {
let selected = |flag: &&FlagMeta<'a>| {
let carried = crate::is_version_flag(flag.flag)
|| (flag.flag.global
&& (view.all_globals
|| view.globals.iter().any(|selector| {
selector
.strip_prefix("--")
.is_some_and(|long| flag.flag.longs.contains(&long))
|| selector
.strip_prefix('-')
.filter(|short| short.len() == 1)
.and_then(|short| short.as_bytes().first().copied())
.is_some_and(|short| flag.flag.shorts.contains(&short))
})));
carried
&& !promoted
.flags
.iter()
.any(|local| crate::spec::flag_forms_overlap(flag.flag, local.flag))
};
let mut flags: Vec<FlagMeta<'a>> = spec.root.flags.iter().filter(selected).copied().collect();
flags.extend_from_slice(promoted.flags);
flags
}
pub(crate) fn view_root_fields<'a>(
spec: &'a Spec<'a>,
promoted: &CommandMeta<'a>,
view: &ViewMeta<'a>,
) -> (Vec<FlagMeta<'a>>, Vec<crate::spec::GroupMeta<'a>>) {
let mut flags = view_root_flags(spec, promoted, view);
let carried = flags.len().saturating_sub(promoted.flags.len());
let matches = |flag: &FlagMeta<'_>, selector: &str| {
selector
.strip_prefix("--")
.is_some_and(|long| flag.flag.longs.contains(&long))
|| selector
.strip_prefix('-')
.filter(|short| short.len() == 1)
.and_then(|short| short.as_bytes().first().copied())
.is_some_and(|short| flag.flag.shorts.contains(&short))
};
let mut groups = Vec::new();
for group in spec.root.groups {
let members: Vec<usize> = group
.members
.iter()
.filter_map(|selector| {
flags[..carried]
.iter()
.position(|flag| matches(flag, selector))
})
.collect();
match members.as_slice() {
[only] if group.required => flags[*only].required = true,
[_, _, ..] => {
groups.push(*group);
}
_ => {}
}
}
groups.extend_from_slice(promoted.groups);
(flags, groups)
}
fn recursive_help<'a>(
spec: &'a Spec<'a>,
path: Vec<&'a str>,
chain: Vec<&'a CommandMeta<'a>>,
style: Style,
inherit_version_actions: bool,
) -> String {
fn append<'a>(
out: &mut String,
spec: &'a Spec<'a>,
path: &mut Vec<&'a str>,
chain: &mut Vec<&'a CommandMeta<'a>>,
style: Style,
inherit_version_actions: bool,
) {
if !out.is_empty() {
out.push('\n');
}
out.push_str(&assembled_help(
spec,
path,
chain,
true,
style,
inherit_version_actions,
));
let current = *chain.last().expect("a recursive page has a command");
let mut children: Vec<_> = current.subcommands.iter().filter(|cmd| !cmd.hide).collect();
children.sort_unstable_by_key(|cmd| (cmd.display_order.unwrap_or(999), cmd.cmd.name));
for child in children {
path.push(child.cmd.name);
chain.push(child);
append(out, spec, path, chain, style, inherit_version_actions);
chain.pop();
path.pop();
}
}
let mut out = String::new();
let mut path = path;
let mut chain = chain;
append(
&mut out,
spec,
&mut path,
&mut chain,
style,
inherit_version_actions,
);
out
}
#[cfg(test)]
mod style_tests {
use super::{
commands_section, display_usage_masked, flag_notes, flag_usage, flat_commands_short,
inline_environment_notes, long_help, render_styled, render_view_at_styled,
styled_flag_usage, styled_help, styled_inline, wrap, Shown, Style,
};
use crate::spec::{ArgMeta, CommandMeta, FlagMeta, Spec, ViewMeta};
use crate::{Arg, ArgAction, Command, Flag};
#[test]
fn nested_lists_keep_their_hanging_indent() {
assert_eq!(
wrap(" - a nested item with enough words to wrap", 24),
[" - a nested item with", " enough words to wrap"]
);
assert_eq!(
wrap(" 1. a numbered item with enough words to wrap", 24),
[
" 1. a numbered item",
" with enough words",
" to wrap"
]
);
}
#[test]
fn optional_equals_values_put_the_equals_inside_the_brackets() {
let flag = Flag {
name: "color",
longs: &["color"],
require_equals: true,
..Flag::VALUE
};
let meta = FlagMeta {
flag: &flag,
value_name: Some("WHEN"),
value_optional: true,
..FlagMeta::EMPTY
};
assert_eq!(flag_usage(&meta), "--color[=WHEN]");
}
#[test]
fn a_negation_left_after_positive_spellings_are_masked_keeps_its_flag_name() {
let flag = Flag {
name: "color",
shorts: b"c",
longs: &["color"],
negate: Some("no-color"),
..Flag::BOOL
};
let meta = FlagMeta {
flag: &flag,
..FlagMeta::EMPTY
};
let shown = Shown {
long: None,
short: None,
negate: true,
};
assert_eq!(display_usage_masked(&meta, &shown), "color: --no-color");
}
#[test]
fn a_flag_spelled_only_as_its_negation_writes_that_spelling_and_nothing_before_it() {
let flag = Flag {
name: "no-credit",
negate: Some("no-credit"),
..Flag::BOOL
};
let meta = FlagMeta {
flag: &flag,
..FlagMeta::EMPTY
};
let shown = Shown {
long: None,
short: None,
negate: true,
};
assert_eq!(display_usage_masked(&meta, &shown), "--no-credit");
}
#[test]
fn flattened_next_line_deprecation_follows_help_without_a_blank_row() {
let flag = Flag {
name: "old",
longs: &["old"],
..Flag::BOOL
};
let flag_meta = FlagMeta {
flag: &flag,
help: Some("Use the old mode"),
deprecated: Some("use --new"),
..FlagMeta::EMPTY
};
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
flags: &[flag_meta],
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
next_line_help: true,
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();
flat_commands_short(&mut page, &["tool"], &root_meta, 80);
assert!(
page.contains(" Use the old mode\n [deprecated: use --new]"),
"{page}"
);
assert!(!page.contains("Use the old mode\n\n [deprecated"));
}
#[test]
fn flattened_next_line_flags_without_help_still_end_their_usage_rows() {
let old = Flag {
name: "old",
longs: &["old"],
..Flag::BOOL
};
let new = Flag {
name: "new",
longs: &["new"],
..Flag::BOOL
};
let flags = [
FlagMeta {
flag: &old,
deprecated: Some("use --new"),
..FlagMeta::EMPTY
},
FlagMeta {
flag: &new,
..FlagMeta::EMPTY
},
];
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
flags: &flags,
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
next_line_help: true,
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();
flat_commands_short(&mut page, &["tool"], &root_meta, 80);
assert!(page.contains("--old\n"), "{page}");
assert!(page.contains("[deprecated: use --new]\n"), "{page}");
assert!(page.contains("--new\n"), "{page}");
assert!(!page.contains("--old [deprecated"), "{page}");
}
#[test]
fn hidden_environment_names_include_fallbacks_and_deprecated_aliases() {
let flag = Flag {
name: "token",
..Flag::BOOL
};
let meta = FlagMeta {
flag: &flag,
hide_env: true,
env_fallback: &["OLD_TOKEN"],
deprecated_env: &["LEGACY_TOKEN"],
..FlagMeta::EMPTY
};
let mut page = String::new();
flag_notes(&mut page, &meta, 4, 80);
assert!(page.is_empty());
let visible = inline_environment_notes(false, &["OLD_TOKEN"], &["LEGACY_TOKEN"])
.expect("visible environment notes");
assert!(visible.contains("[env fallback: OLD_TOKEN]"));
assert!(visible.contains("[deprecated env: LEGACY_TOKEN]"));
assert!(inline_environment_notes(true, &["OLD_TOKEN"], &["LEGACY_TOKEN"]).is_none());
}
#[test]
fn short_command_rows_trim_trailing_help_whitespace() {
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
about: Some("run it\n"),
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();
commands_section(&mut page, &[], &root_meta, 80, false);
assert!(page.contains(" run run it\n help"));
assert!(!page.contains(" run run it\n\n help"));
}
#[test]
fn long_help_preserves_configured_spacing_before_package_metadata() {
let command = Command {
name: "ex",
..Command::EMPTY
};
let root = CommandMeta {
cmd: &command,
after_help: Some("More help.\n"),
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
author: Some("Example Author"),
root: &root,
..Spec::EMPTY
};
let page = long_help(&spec, &["ex"], &[&root]);
assert!(
page.contains("More help.\n\n\nAuthor: Example Author\n"),
"{page}"
);
}
#[test]
fn view_help_keeps_declared_and_synthesized_host_version_actions() {
let build_info = Flag {
name: "build-info",
longs: &["build-info"],
action: ArgAction::Version,
..Flag::BOOL
};
let nested_command = Command {
name: "status",
..Command::EMPTY
};
let child_command = Command {
name: "serve",
subcommands: &[&nested_command],
..Command::EMPTY
};
let root_command = Command {
name: "host",
flags: &[&build_info],
subcommands: &[&child_command],
version: true,
..Command::EMPTY
};
let build_info_meta = FlagMeta {
flag: &build_info,
help: Some("Print build information"),
..FlagMeta::EMPTY
};
let nested_meta = CommandMeta {
cmd: &nested_command,
..CommandMeta::EMPTY
};
let child_meta = CommandMeta {
cmd: &child_command,
subcommands: &[&nested_meta],
..CommandMeta::EMPTY
};
let root_meta = CommandMeta {
cmd: &root_command,
flags: &[build_info_meta],
subcommands: &[&child_meta],
..CommandMeta::EMPTY
};
let spec = Spec {
name: "host",
bin: Some("host"),
root: &root_meta,
..Spec::EMPTY
};
let view = ViewMeta {
id: "server",
name: "server",
bin: "server",
root: "serve",
all_globals: false,
globals: &[],
};
let page = render_view_at_styled(
&spec,
&[&root_command, &child_command, &nested_command],
&view,
false,
Style::PLAIN,
)
.expect("view route");
assert!(page.contains("--build-info"), "{page}");
assert!(page.contains("-V, --version"), "{page}");
}
#[test]
fn coloured_help_styles_structure_without_changing_plain_text() {
let page = "A summary ending in:\nUsage: prose is not a synopsis\nExamples:\n\nUsage: ex [OPTIONS]\n ex --all\n\nArguments:\n <FILE> Read this file\n\nOptions:\n -f, --force Force it\n [possible values: --auto]\n (default: -1)\n";
let headings = vec!["Arguments".to_string(), "Options".to_string()];
let usages = vec!["-f, --force".to_string()];
let arg_usages = vec!["<FILE>".to_string()];
let synopsis = vec![
"Usage: ex [OPTIONS]".to_string(),
" ex --all".to_string(),
];
assert_eq!(
styled_help(
page,
Style::PLAIN,
&headings,
&usages,
&arg_usages,
&synopsis
),
page
);
let coloured = styled_help(
page,
Style::COLOURED,
&headings,
&usages,
&arg_usages,
&synopsis,
);
assert!(coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[1;33mOptions:\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[1;32m-f\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[1;32m--force\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[1;35m<FILE>\u{1b}[0m"));
assert!(coloured.contains("A summary ending in:\nUsage: prose is not a synopsis"));
assert!(coloured.contains("Usage: prose is not a synopsis\nExamples:"));
assert!(coloured.contains("ex \u{1b}[1;32m--all\u{1b}[0m"));
assert!(coloured.contains("[possible values: --auto]"));
assert!(coloured.contains("(default: -1)"));
assert_eq!(strip_ansi(&coloured), page);
}
#[test]
fn a_help_template_can_style_its_own_text_without_styling_plain_output() {
let force = Flag {
name: "force",
longs: &["force"],
..Flag::BOOL
};
let command = Command {
name: "ex",
flags: &[&force],
..Command::EMPTY
};
let root = CommandMeta {
cmd: &command,
flags: &[FlagMeta {
flag: &force,
help: Some("Do it anyway"),
..FlagMeta::EMPTY
}],
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
help_template: Some("{$heading}CUSTOM HELP{/$}\n\n{{usage}}\n\n{$cyan}{{flags}}{/$}"),
root: &root,
..Spec::EMPTY
};
let plain = render_styled(&spec, &command, false, Style::PLAIN).expect("root page");
assert!(plain.starts_with("CUSTOM HELP\n\nUsage: ex"), "{plain}");
assert!(!plain.contains("{$"), "{plain}");
let coloured = render_styled(&spec, &command, false, Style::COLOURED).expect("root page");
assert!(coloured.starts_with("\u{1b}[1;33mCUSTOM HELP\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[36m\u{1b}[1;33mFlags:"));
assert_eq!(strip_ansi(&coloured), plain);
let malformed = Spec {
name: "ex",
help_template: Some("before {$red and {{usage}}"),
root: &root,
..Spec::EMPTY
};
let page = render_styled(&malformed, &command, false, Style::COLOURED)
.expect("a programmatic malformed template remains renderable");
assert!(page.starts_with("before {$red and \u{1b}[1;33mUsage:"));
}
#[test]
fn equals_separates_a_coloured_flag_from_its_value() {
assert_eq!(
styled_flag_usage("--output=<FILE>", Style::COLOURED),
"\u{1b}[1;32m--output\u{1b}[0m=\u{1b}[1;35m<FILE>\u{1b}[0m"
);
assert_eq!(
styled_flag_usage("--color[=WHEN]", Style::COLOURED),
"\u{1b}[1;32m--color\u{1b}[0m[=\u{1b}[1;35mWHEN\u{1b}[0m]"
);
assert_eq!(
styled_flag_usage("<--output <OUTPUT>>", Style::COLOURED),
"<\u{1b}[1;32m--output\u{1b}[0m \u{1b}[1;35m<OUTPUT>\u{1b}[0m>"
);
}
#[test]
fn metavar_scanning_handles_lowercase_capitalized_and_unicode_words() {
assert_eq!(
styled_flag_usage("ex [file]", Style::COLOURED),
"ex [\u{1b}[1;35mfile\u{1b}[0m]"
);
assert_eq!(
styled_flag_usage("ex Add [ÜBERSICHT]", Style::COLOURED),
"ex Add [\u{1b}[1;35mÜBERSICHT\u{1b}[0m]"
);
assert_eq!(
styled_flag_usage("ex TOOL@VERSION", Style::COLOURED),
"ex \u{1b}[1;35mTOOL@VERSION\u{1b}[0m"
);
}
#[test]
fn flattened_descendant_rows_receive_argument_and_flag_styles() {
let file = Arg {
name: "file",
..Arg::REQUIRED
};
let force = Flag {
name: "force",
longs: &["force"],
..Flag::BOOL
};
let run = Command {
name: "run",
args: &[&file],
flags: &[&force],
..Command::EMPTY
};
let root_command = Command {
name: "ex",
subcommands: &[&run],
..Command::EMPTY
};
let run_meta = CommandMeta {
cmd: &run,
args: &[ArgMeta {
arg: &file,
help: Some("A file"),
required: false,
..ArgMeta::EMPTY
}],
flags: &[FlagMeta {
flag: &force,
help: Some("Force it"),
..FlagMeta::EMPTY
}],
..CommandMeta::EMPTY
};
let root_meta = CommandMeta {
cmd: &root_command,
flatten_help: true,
subcommands: &[&run_meta],
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
root: &root_meta,
..Spec::EMPTY
};
let page = render_styled(&spec, &root_command, false, Style::COLOURED).expect("root help");
assert!(page.contains("[\u{1b}[1;35mfile\u{1b}[0m]"), "{page:?}");
assert!(page.contains("\u{1b}[1;32m--force\u{1b}[0m"), "{page:?}");
}
#[test]
fn coloured_help_renders_inline_markdown_emphasis() {
let page = "Use **force** for *all* files, _including_hidden_, `--literally`, and ~~never~~ this.\n --dry_run Keep snake_case and an unmatched * glob\n\nExamples:\n $ echo `date`\n";
let coloured = styled_help(page, Style::COLOURED, &[], &[], &[], &[]);
assert!(
coloured.contains("\u{1b}[1mforce\u{1b}[22m"),
"{coloured:?}"
);
assert!(coloured.contains("\u{1b}[3mall\u{1b}[23m"), "{coloured:?}");
assert!(
coloured.contains("\u{1b}[3mincluding_hidden\u{1b}[23m"),
"{coloured:?}"
);
assert!(
coloured.contains("\u{1b}[36m--literally\u{1b}[39m"),
"{coloured:?}"
);
assert!(
coloured.contains("\u{1b}[9mnever\u{1b}[29m"),
"{coloured:?}"
);
assert!(coloured.contains("--dry_run Keep snake_case and an unmatched * glob"));
assert!(coloured.contains(" $ echo `date`"));
assert!(!coloured.contains("**force**"));
}
#[test]
fn inline_emphasis_nests_and_can_be_escaped() {
assert_eq!(
styled_inline("**bold and *italic*** plus \\*literal\\*", None),
"\u{1b}[1mbold and \u{1b}[3mitalic\u{1b}[23m\u{1b}[1m\u{1b}[22m plus *literal*"
);
assert_eq!(
styled_inline("*italic and **bold***", None),
"\u{1b}[3mitalic and \u{1b}[1mbold\u{1b}[22m\u{1b}[3m\u{1b}[23m"
);
assert_eq!(
styled_inline("_italic and __bold___", None),
"\u{1b}[3mitalic and \u{1b}[1mbold\u{1b}[22m\u{1b}[3m\u{1b}[23m"
);
}
#[test]
fn intraword_underscore_runs_remain_literal() {
assert_eq!(
styled_inline("foo__bar__ foo___bar___ baz_qux", None),
"foo__bar__ foo___bar___ baz_qux"
);
}
#[test]
fn an_escape_skips_one_closing_marker() {
assert_eq!(
styled_inline("*italic \\**", None),
"\u{1b}[3mitalic *\u{1b}[23m"
);
assert_eq!(
styled_inline("**bold \\***", None),
"\u{1b}[1mbold *\u{1b}[22m"
);
}
#[test]
fn a_shared_delimiter_run_is_bold_and_italic() {
assert_eq!(
styled_inline("***combined***", None),
"\u{1b}[1;3mcombined\u{1b}[22;23m"
);
assert_eq!(
styled_inline("___combined___", None),
"\u{1b}[1;3mcombined\u{1b}[22;23m"
);
}
#[test]
fn combined_emphasis_can_nest_in_single_emphasis() {
assert_eq!(
styled_inline("*italic ***combined*** tail*", None),
"\u{1b}[3mitalic \u{1b}[1;3mcombined\u{1b}[22;23m\u{1b}[3m tail\u{1b}[23m"
);
assert_eq!(
styled_inline("**bold ***combined*** tail**", None),
"\u{1b}[1mbold \u{1b}[1;3mcombined\u{1b}[22;23m\u{1b}[1m tail\u{1b}[22m"
);
}
#[test]
fn a_closing_run_remainder_can_open_an_adjacent_span() {
assert_eq!(
styled_inline("*italic***bold**", None),
"\u{1b}[3mitalic\u{1b}[23m\u{1b}[1mbold\u{1b}[22m"
);
assert_eq!(
styled_inline("**bold***italic*", None),
"\u{1b}[1mbold\u{1b}[22m\u{1b}[3mitalic\u{1b}[23m"
);
}
fn strip_ansi(text: &str) -> String {
let mut out = String::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\u{1b}' && chars.peek() == Some(&'[') {
chars.next();
for code in chars.by_ref() {
if code == 'm' {
break;
}
}
} else {
out.push(ch);
}
}
out
}
}
#[cfg(test)]
#[test]
fn an_overflowing_entry_wraps_even_when_the_page_is_very_narrow() {
let mut page = String::new();
let width = 10;
let col = usage_column_width("--long".chars().count(), width);
entry(&mut page, "--long", Some("alpha beta"), col, width, false);
assert_eq!(page, " --long\n alpha\n beta\n");
}