use core::fmt::Write as _;
use std::borrow::Cow;
use crate::order::sort_by as sort_rows;
use crate::spec::{
AdmonitionKind, AdmonitionMeta, ArgMeta, CommandMeta, Example, FlagMeta, Spec, ViewMeta,
};
use crate::Command;
use crate::DoubleDash;
mod template;
pub use template::STYLES;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Palette {
pub heading: &'static str,
pub option: &'static str,
pub metavar: &'static str,
pub command: &'static str,
}
impl Palette {
pub const DEFAULT: Palette = Palette {
heading: "heading",
option: "option",
metavar: "metavar",
command: "command",
};
pub const fn heading(mut self, spec: &'static str) -> Self {
self.heading = spec;
self
}
pub const fn option(mut self, spec: &'static str) -> Self {
self.option = spec;
self
}
pub const fn metavar(mut self, spec: &'static str) -> Self {
self.metavar = spec;
self
}
pub const fn command(mut self, spec: &'static str) -> Self {
self.command = spec;
self
}
}
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, |name| self.named(name))
}
}
fn assemble(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
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(),
};
with_logo(spec, path, chain, style, finish_page(page, style))
}
const LOGO_GUTTER: usize = 2;
const LOGO_MIN_PAGE: usize = 50;
fn logo_margin(spec: &Spec<'_>, root: bool, width: usize) -> Option<(usize, usize)> {
let logo = spec.logo.filter(|_| root)?;
let art = logo_lines(logo);
if art.is_empty() || width == usize::MAX {
return None;
}
let art_width = art.iter().copied().map(shown_width).max().unwrap_or(0);
let page = width.checked_sub(art_width + LOGO_GUTTER)?;
(page >= LOGO_MIN_PAGE).then_some((page, width - art_width))
}
fn page_width(spec: &Spec<'_>, path: &[&str], meta: &CommandMeta<'_>) -> usize {
let width = terminal_width(meta);
match logo_margin(spec, path.len() <= 1, width) {
Some((page, _)) => page,
None => width,
}
}
fn with_logo(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
style: Style,
page: String,
) -> String {
let Some(logo) = spec.logo.filter(|_| path.len() <= 1) else {
return page;
};
let Some(meta) = chain.last() else {
return page;
};
let width = terminal_width(meta);
place_logo(
&page,
logo,
spec.logo_style,
width,
logo_margin(spec, true, width).map(|(_, column)| column),
style.coloured,
)
}
pub fn place_logo(
page: &str,
logo: &str,
style: Option<&str>,
width: usize,
reserved: Option<usize>,
coloured: bool,
) -> String {
let art = logo_lines(logo);
if art.is_empty() {
return page.to_string();
}
let art_width = art.iter().copied().map(shown_width).max().unwrap_or(0);
let column = reserved.filter(|column| {
page.lines()
.take(art.len())
.all(|line| shown_width(line) + LOGO_GUTTER <= *column)
});
let mut out = match column {
Some(column) => {
let mut lines: Vec<String> = page.lines().map(str::to_string).collect();
if lines.len() < art.len() {
lines.resize(art.len(), String::new());
}
for (line, art) in lines.iter_mut().zip(&art) {
if art.is_empty() {
continue;
}
for _ in 0..column.saturating_sub(shown_width(line)) {
line.push(' ');
}
line.push_str(&painted_logo(art, style, coloured));
}
lines.join("\n")
}
None if art_width <= width => {
let mut banner = String::new();
for art in &art {
banner.push_str(&painted_logo(art, style, coloured));
banner.push('\n');
}
banner.push('\n');
banner.push_str(page);
banner
}
None => return page.to_string(),
};
out.truncate(out.trim_end().len());
out.push('\n');
out
}
fn painted_logo(line: &str, style: Option<&str>, coloured: bool) -> String {
if !coloured {
return strip_ansi_sequences(line.to_string());
}
match style {
Some(style) if !line.trim().is_empty() => template::semantic(style, line, Style::COLOURED),
_ => line.to_string(),
}
}
fn logo_lines(logo: &str) -> Vec<&str> {
let mut lines: Vec<&str> = logo.lines().map(str::trim_end).collect();
while lines.first().is_some_and(|line| line.is_empty()) {
lines.remove(0);
}
while lines.last().is_some_and(|line| line.is_empty()) {
lines.pop();
}
lines
}
fn shown_width(line: &str) -> usize {
let bytes = line.as_bytes();
let mut at = 0;
let mut columns = 0;
while at < bytes.len() {
if bytes[at] == b'\x1b' && bytes.get(at + 1) == Some(&b'[') {
let mut end = at + 2;
while end < bytes.len() && !(0x40..=0x7e).contains(&bytes[end]) {
end += 1;
}
at = (end + 1).min(bytes.len());
continue;
}
if bytes[at] & 0xc0 != 0x80 {
columns += 1;
}
at += 1;
}
columns
}
fn finish_page(page: String, style: Style) -> String {
let page = if style.coloured {
page
} else {
strip_ansi_sequences(page)
};
let trimmed = page.trim();
let mut done = String::with_capacity(trimmed.len() + 1);
done.push_str(trimmed);
done.push('\n');
done
}
fn strip_ansi_sequences(text: String) -> String {
let bytes = text.as_bytes();
let Some(mut at) = bytes.windows(2).position(|pair| pair == b"\x1b[") else {
return text;
};
let mut plain = String::with_capacity(text.len());
let mut copied = 0;
while at + 1 < bytes.len() {
if bytes[at] != b'\x1b' || bytes[at + 1] != b'[' {
at += 1;
continue;
}
let mut end = at + 2;
while end < bytes.len() && (0x30..=0x3f).contains(&bytes[end]) {
end += 1;
}
while end < bytes.len() && (0x20..=0x2f).contains(&bytes[end]) {
end += 1;
}
if end == bytes.len() || !(0x40..=0x7e).contains(&bytes[end]) {
at += 2;
continue;
}
plain.push_str(&text[copied..at]);
copied = end + 1;
at = copied;
}
plain.push_str(&text[copied..]);
plain
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Style {
coloured: bool,
palette: Palette,
}
impl Style {
pub const PLAIN: Style = Style {
coloured: false,
palette: Palette::DEFAULT,
};
pub const COLOURED: Style = Style {
coloured: true,
palette: Palette::DEFAULT,
};
pub(crate) const fn coloured_if(coloured: bool) -> Style {
Style {
coloured,
palette: Palette::DEFAULT,
}
}
pub const fn palette(self, palette: Palette) -> Self {
Style { palette, ..self }
}
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)
}
fn literal(self, text: &str) -> String {
template::semantic("option", text, self)
}
fn metavar(self, text: &str) -> String {
template::semantic("metavar", text, self)
}
fn command(self, text: &str) -> String {
template::semantic("command", text, self)
}
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
}
#[doc(hidden)]
pub const fn __usage_advanced_help(enabled: bool) -> bool {
assert!(
!enabled || cfg!(feature = "help-advanced"),
"flatten_help and HelpAll require the `help-advanced` feature"
);
enabled && cfg!(feature = "help-advanced")
}
fn flatten_help(meta: &CommandMeta<'_>) -> bool {
__usage_advanced_help(meta.extra.flatten_help)
}
const STRUCTURE: char = '\u{1}';
fn write_heading(out: &mut String, title: &str, style: Style) {
out.push('\n');
if style.coloured {
out.push(STRUCTURE);
out.push_str(&style.heading(&format!("{title}:")));
} else {
out.push_str(title);
out.push(':');
}
out.push('\n');
}
fn painted_usage(usage: &str, style: Style) -> Cow<'_, str> {
if style.coloured {
Cow::Owned(styled_flag_usage(usage, style))
} else {
Cow::Borrowed(usage)
}
}
fn paint_synopsis(usage: &mut String, style: Style) {
if !style.coloured {
return;
}
let mut out = String::with_capacity(usage.len() * 2);
for line in usage.split_inclusive('\n') {
let (body, newline) = line
.strip_suffix('\n')
.map_or((line, ""), |body| (body, "\n"));
out.push(STRUCTURE);
match body.strip_prefix("Usage:") {
Some(rest) => {
out.push_str(&style.heading("Usage:"));
out.push_str(&styled_flag_usage(rest, style));
}
None => out.push_str(&styled_flag_usage(body, style)),
}
out.push_str(newline);
}
*usage = out;
}
fn painted_prose(page: String, style: Style) -> String {
if !style.coloured {
return page;
}
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 let Some(structure) = body.strip_prefix(STRUCTURE) {
out.push_str(structure);
} else if body.trim_start().starts_with("$ ") {
out.push_str(body);
} else {
out.push_str(&style.inline(body));
}
out.push_str(newline);
}
out
}
fn rendered_page(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
style: Style,
inherit_version_actions: bool,
suppress_global: bool,
) -> String {
let sections = page_sections(
spec,
path,
chain,
long,
inherit_version_actions,
suppress_global,
style,
);
let page = match spec
.help_template
.filter(|template| !template.trim().is_empty())
{
Some(template) => template::substitute(template, style, |name| {
sections.named(name).map(|part| painted_prose(part, style))
}),
None => painted_prose(sections.concatenated(), style),
};
with_logo(spec, path, chain, style, finish_page(page, style))
}
fn assembled_help(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
style: Style,
inherit_version_actions: bool,
include_default_help: bool,
) -> String {
let page = rendered_page(
spec,
path,
chain,
long,
style,
inherit_version_actions,
false,
);
if include_default_help {
with_default_command_help(
spec,
path,
chain,
long,
style,
inherit_version_actions,
page,
)
} else {
page
}
}
pub fn usage_line(path: &[&str], meta: &CommandMeta<'_>) -> String {
usage_line_with_subcommands(path, meta, true)
}
fn positional_args<'a>(meta: &'a CommandMeta<'a>) -> &'a [ArgMeta<'a>] {
meta.extra.clause.map_or(meta.args, |clause| clause.args)
}
fn is_clause_flag(meta: &CommandMeta<'_>, flag: &FlagMeta<'_>) -> bool {
meta.extra.clause.is_some_and(|clause| {
clause
.flags
.iter()
.any(|scoped| core::ptr::eq(scoped.flag, flag.flag))
})
}
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 command_flags = meta
.flags
.iter()
.filter(|f| !f.hide && !f.builtin && !is_clause_flag(meta, f));
let flags = command_flags.clone().count();
if flags > 0 {
let required = command_flags.clone().any(flag_demanded);
if flags <= INLINE_LIMIT {
for flag in command_flags {
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 positional_args = positional_args(meta);
let args: usize = positional_args.iter().filter(|a| !a.hide).count();
if args > 0 {
let required = positional_args.iter().any(|a| !a.hide && demanded(a));
if let Some(clause) = meta.extra.clause {
let inner = positional_args
.iter()
.filter(|a| !a.hide)
.map(arg_usage)
.collect::<Vec<_>>()
.join(" ");
match clause.separator {
Some(separator) => {
let _ = write!(out, " [{inner} [{separator} {inner}]…]");
}
None => {
let arg = positional_args
.iter()
.find(|arg| !arg.hide)
.expect("an implicit clause has one visible positional");
let separator = if arg.arg.double_dash == DoubleDash::Required {
"-- "
} else {
""
};
let _ = write!(out, " [{separator}{}]…", arg.arg.name);
}
}
} else if args <= INLINE_LIMIT {
for arg in positional_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.extra.subcommand_value_name.unwrap_or("SUBCOMMAND");
if meta.extra.subcommand_required {
let _ = write!(out, " <{name}>");
} else {
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;
}
}
if !flatten_help(meta) || !meta.subcommands.iter().any(|sub| !sub.hide) {
let _ = writeln!(out, "Usage: {}", usage_line(path, meta));
return;
}
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
fn compare_names(a: &CommandMeta<'_>, b: &CommandMeta<'_>) -> core::cmp::Ordering {
a.cmd.name.cmp(b.cmd.name)
}
sort_rows(&mut visible, &mut |a, b| compare_names(a, b));
let mut lines = Vec::new();
if !meta.extra.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}");
}
}
}
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.extra.hidden_longs.contains(long)),
short: meta
.flag
.shorts
.iter()
.copied()
.find(|short| !meta.extra.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.extra.hidden_longs.contains(l) && !taken.contains(&format!("--{l}"))
}),
short: meta.flag.shorts.iter().copied().find(|s| {
!meta.extra.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.extra.value_var_min, meta.extra.value_var_max);
if meta.extra.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
let name = meta
.extra
.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.extra.value_names.len() <= 1 {
let name = meta
.extra
.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.extra.value_names.iter().enumerate() {
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
index == 0,
);
}
}
if flag.variadic && meta.extra.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<u32>, max: Option<u32>) -> Option<usize> {
match (min, max) {
(Some(min), Some(max)) if min == max => Some(min as usize),
_ => None,
}
}
pub fn short_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) -> String {
with_default_command_help(
spec,
path,
chain,
false,
Style::PLAIN,
false,
short_help_with(spec, path, chain, false),
)
}
fn short_help_with(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> String {
assemble(
spec,
path,
chain,
&page_sections(
spec,
path,
chain,
false,
inherit_version_actions,
false,
Style::PLAIN,
),
Style::PLAIN,
)
}
fn hidden_on(long: bool, hide_short_help: bool, hide_long_help: bool) -> bool {
if long {
hide_long_help
} else {
hide_short_help
}
}
fn page_sections(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
inherit_version_actions: bool,
suppress_global: bool,
style: Style,
) -> 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| !hidden_on(long, flag.hide_short_help, flag.hide_long_help))
.collect();
let inherited: Vec<_> = if suppress_global {
Vec::new()
} else {
inherited
.into_iter()
.filter(|(flag, _)| !hidden_on(long, flag.hide_short_help, flag.hide_long_help))
.collect()
};
let mut sections = Sections::default();
let width = page_width(spec, path, meta);
let out = &mut sections.about;
let before = if long {
meta.extra.before_long_help.or(meta.extra.before_help)
} else {
meta.extra.before_help
};
if let Some(before) = before {
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 = match (root, long) {
(true, true) => spec.long_about.or(spec.about),
(true, false) => spec.about,
(false, true) => meta.long_about.or(meta.about),
(false, false) => 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);
paint_synopsis(&mut sections.usage, style);
if !flatten_help(meta) {
commands_section(
&mut sections.commands,
&path[1.min(path.len())..],
meta,
width,
long,
root_default_command_name(spec, path, meta),
style,
);
}
let positional_args = positional_args(meta);
let mut args: Vec<&ArgMeta<'_>> = positional_args
.iter()
.filter(|a| !a.hide && !hidden_on(long, a.hide_short_help, a.hide_long_help))
.collect();
order_args(&mut args, positional_args);
let args: Vec<(&ArgMeta<'_>, String)> = args.into_iter().map(|a| (a, arg_usage(a))).collect();
let own: Vec<(&FlagMeta<'_>, String)> = own.into_iter().map(|f| (f, column_usage(f))).collect();
let arg_col = args
.iter()
.map(|(_, usage)| usage.chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let prose_of = |title: &str| {
if long {
heading_help(meta, title)
} else {
None
}
};
let layout = RowLayout {
col: arg_col,
width,
next_line: meta.extra.next_line_help,
long,
aligned: true,
style,
};
split_groups_section(
SectionSink {
page: &mut sections.args,
ungrouped: &mut sections.ungrouped_args,
grouped: &mut sections.grouped_args,
style,
},
"Arguments",
width,
&args,
|(a, _)| a.help_heading,
prose_of,
|out, (a, usage)| write_row(out, &Row::arg(a, usage), layout),
);
let flag_col = own
.iter()
.chain(&inherited)
.map(|(_, usage)| usage.chars().count())
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let layout = RowLayout {
col: flag_col,
..layout
};
split_groups_section(
SectionSink {
page: &mut sections.flags,
ungrouped: &mut sections.ungrouped_flags,
grouped: &mut sections.grouped_flags,
style,
},
"Flags",
width,
&own,
|(f, _)| flag_help_heading(meta, f),
prose_of,
|out, (f, usage)| write_row(out, &Row::flag(f, usage), layout),
);
split_groups_section(
SectionSink {
page: &mut sections.flags,
ungrouped: &mut sections.ungrouped_flags,
grouped: &mut sections.grouped_flags,
style,
},
"Global flags",
width,
&inherited,
|_| None,
|_| None,
|out, (f, usage)| write_row(out, &Row::flag(f, usage), layout),
);
if flatten_help(meta) {
flat_commands(
&mut sections.flattened,
&path[1.min(path.len())..],
meta,
width,
long,
style,
);
}
let out = &mut sections.after_help;
examples_section(out, meta.extra.examples, long, style);
let after = if long {
meta.extra.after_long_help.or(meta.extra.after_help)
} else {
meta.extra.after_help
};
if let Some(after) = after {
out.push('\n');
write_wrapped_indented(out, after, width, 0);
}
if long && root && (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
}
struct Row<'a> {
usage: &'a str,
help: Option<&'a str>,
long_help: Option<&'a str>,
choices: &'a [&'a str],
env: Option<&'a str>,
env_fallback: &'a [&'a str],
deprecated_env: &'a [&'a str],
default: &'a [&'a str],
admonitions: &'a [AdmonitionMeta<'a>],
deprecated: Option<&'a str>,
deprecated_warn_at: Option<&'a str>,
deprecated_remove_at: Option<&'a str>,
}
impl<'a> Row<'a> {
fn arg(meta: &'a ArgMeta<'a>, usage: &'a str) -> Self {
Row {
usage,
help: meta.help,
long_help: meta.long_help,
choices: if meta.hide_possible_values {
&[]
} else {
meta.choices
},
env: if meta.hide_env { None } else { meta.env },
env_fallback: if meta.hide_env {
&[]
} else {
meta.env_fallback
},
deprecated_env: if meta.hide_env {
&[]
} else {
meta.deprecated_env
},
default: if meta.hide_default_value {
&[]
} else {
meta.default
},
admonitions: meta.admonitions,
deprecated: None,
deprecated_warn_at: None,
deprecated_remove_at: None,
}
}
fn flag(meta: &'a FlagMeta<'a>, usage: &'a str) -> Self {
let extra = meta.extra;
Row {
usage,
help: meta.help,
long_help: meta.long_help,
choices: if meta.hide_possible_values {
&[]
} else {
meta.choices
},
env: if meta.hide_env { None } else { meta.env },
env_fallback: if meta.hide_env {
&[]
} else {
extra.env_fallback
},
deprecated_env: if meta.hide_env {
&[]
} else {
extra.deprecated_env
},
default: if meta.hide_default_value {
&[]
} else {
meta.default
},
admonitions: extra.admonitions,
deprecated: extra.deprecated,
deprecated_warn_at: extra.deprecated_warn_at,
deprecated_remove_at: extra.deprecated_remove_at,
}
}
fn deprecation(&self) -> Option<String> {
deprecation_label(
self.deprecated,
self.deprecated_warn_at,
self.deprecated_remove_at,
)
}
}
#[derive(Clone, Copy)]
struct RowLayout {
col: usize,
width: usize,
next_line: bool,
long: bool,
aligned: bool,
style: Style,
}
#[inline(never)]
fn write_row(out: &mut String, row: &Row<'_>, layout: RowLayout) {
let RowLayout {
col,
width,
next_line,
long,
aligned,
style,
} = layout;
let painted = painted_usage(row.usage, style);
let notes_at = if long {
let indent = entry(
out,
row.usage,
&painted,
row.long_help.or(row.help),
col,
width,
next_line,
);
admonitions(out, row.admonitions, width);
if aligned {
indent
} else {
BLOCK_INDENT
}
} else if next_line {
let _ = writeln!(out, " {painted}");
if let Some(help) = row.help.filter(|h| !h.trim().is_empty()) {
write_wrapped_block(out, help, width);
}
BLOCK_INDENT
} else {
let deprecation = row.deprecation();
let environment = inline_environment_notes(row.env_fallback, row.deprecated_env);
let notes = inline_annotations(
row.choices,
row.env,
environment.as_deref(),
row.default,
deprecation.as_deref(),
);
entry(
out,
row.usage,
&painted,
with_annotations(row.help, notes).as_deref(),
col,
width,
false,
);
return;
};
long_annotations(
out,
row,
AnnotationLayout {
indent: notes_at,
width,
},
);
if let Some(label) = row.deprecation() {
write_wrapped_indented(out, &label, width, notes_at);
}
}
const HELP_SUBCOMMAND: &str = "help";
const HELP_SUBCOMMAND_SUMMARY: &str = "Print this message or the help of the given subcommand(s)";
fn default_visible_child<'a>(
spec: &Spec<'a>,
root: &'a CommandMeta<'a>,
) -> Option<&'a CommandMeta<'a>> {
let name = spec.default_subcommand?;
root.subcommands
.iter()
.copied()
.find(|sub| sub.cmd.name == name)
.or_else(|| {
root.subcommands
.iter()
.copied()
.find(|sub| sub.cmd.aliases.contains(&name))
})
.filter(|sub| !sub.hide)
}
fn root_default_command_name<'a>(
spec: &Spec<'a>,
path: &[&str],
meta: &'a CommandMeta<'a>,
) -> Option<&'a str> {
(path.len() <= 1)
.then(|| default_visible_child(spec, meta))
.flatten()
.map(|child| child.cmd.name)
}
fn with_default_command_help(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
style: Style,
inherit_version_actions: bool,
parent: String,
) -> String {
if path.len() > 1 || !spec.default_subcommand_help {
return parent;
}
let Some(root) = chain.first().copied() else {
return parent;
};
if flatten_help(root) {
return parent;
}
let Some(child) = default_visible_child(spec, root) else {
return parent;
};
let mut child_path = path.to_vec();
child_path.push(child.cmd.name);
let child_chain = [root, child];
let child_page = rendered_page(
spec,
&child_path,
&child_chain,
long,
style,
inherit_version_actions,
true,
);
let mut out = parent.trim_end().to_string();
out.push_str("\n\nDefault command: ");
out.push_str(&style.command(child.cmd.name));
out.push_str("\n Unmatched words select this command.\n\n");
out.push_str(child_page.trim_end());
out.push('\n');
out
}
fn commands_section(
out: &mut String,
path: &[&str],
meta: &CommandMeta<'_>,
width: usize,
long: bool,
default_name: Option<&str>,
style: Style,
) {
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();
sort_rows(&mut lines, &mut |a, b| {
a.1.extra
.display_order
.unwrap_or(999)
.cmp(&b.1.extra.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.extra.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 {
write_heading(out, heading.unwrap_or(default_title), style);
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,
&style.command(sub.cmd.name),
command_row(sub, default_name == Some(sub.cmd.name)).as_deref(),
col,
width,
meta.extra.next_line_help,
);
}
if heading.is_none() && show_help {
entry(
out,
HELP_SUBCOMMAND,
&style.command(HELP_SUBCOMMAND),
Some(HELP_SUBCOMMAND_SUMMARY),
col,
width,
meta.extra.next_line_help,
);
}
}
}
fn command_row<'a>(sub: &'a CommandMeta<'a>, is_default: bool) -> 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.extra.hidden_aliases.contains(a))
.peekable();
let label = deprecation_label(
sub.extra.deprecated,
sub.extra.deprecated_warn_at,
sub.extra.deprecated_remove_at,
);
if visible_aliases.peek().is_none() && label.is_none() && !is_default {
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);
}
if is_default {
if !row.is_empty() {
row.push(' ');
}
row.push_str("(default)");
}
Some(Cow::Owned(row))
}
fn flat_commands(
out: &mut String,
path: &[&str],
meta: &CommandMeta<'_>,
width: usize,
long: bool,
style: Style,
) {
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);
write_heading(out, &sub_path.join(" "), style);
let about = if long {
sub.long_about.or(sub.about)
} else {
sub.about
};
if let Some(about) = about.filter(|about| !about.trim().is_empty()) {
write_wrapped_indented(out, about.trim_end(), width, 0);
}
command_deprecation(out, sub, 0, width);
let positional_args = positional_args(sub);
let mut args: Vec<_> = positional_args
.iter()
.filter(|arg| !arg.hide && !hidden_on(long, arg.hide_short_help, arg.hide_long_help))
.collect();
order_args(&mut args, positional_args);
let mut flags: Vec<&FlagMeta<'_>> = sub
.flags
.iter()
.filter(|flag| {
!flag.flag.global
&& !flag.hide
&& !hidden_on(long, flag.hide_short_help, 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);
let layout = RowLayout {
col: arg_col,
width,
next_line: meta.extra.next_line_help,
long,
aligned: false,
style,
};
for arg in args {
write_row(out, &Row::arg(arg, &arg_usage(arg)), layout);
}
let layout = RowLayout {
col: flag_col,
..layout
};
for flag in flags {
write_row(out, &Row::flag(flag, &column_usage(flag)), layout);
}
if flatten_help(sub) {
flat_commands(out, &sub_path, sub, width, long, style);
}
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
.extra
.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,
style: Style,
}
fn split_groups_section<'m, T>(
sink: SectionSink<'_>,
default_title: &str,
width: usize,
items: &[T],
heading_of: impl Fn(&T) -> Option<&'m str>,
prose_of: impl Fn(&str) -> Option<&'m str>,
mut write_item: impl FnMut(&mut String, &T),
) {
grouped_sections(
sink,
default_title,
width,
items.len(),
&|index| heading_of(&items[index]),
&prose_of,
&mut |out, index| write_item(out, &items[index]),
);
}
#[inline(never)]
fn grouped_sections<'m>(
sink: SectionSink<'_>,
default_title: &str,
width: usize,
len: usize,
heading_of: &dyn Fn(usize) -> Option<&'m str>,
prose_of: &dyn Fn(&str) -> Option<&'m str>,
write_item: &mut dyn FnMut(&mut String, usize),
) {
let mut headings: Vec<Option<&str>> = Vec::new();
for index in 0..len {
let heading = heading_of(index);
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();
write_heading(&mut section, heading.unwrap_or(default_title), sink.style);
if let Some(prose) = heading.and_then(prose_of) {
write_wrapped_indented(&mut section, prose, width, 2);
section.push('\n');
}
for index in (0..len).filter(|&index| heading_of(index) == heading) {
write_item(&mut section, index);
}
sink.page.push_str(§ion);
match heading {
Some(_) => sink.grouped.push_str(§ion),
None => sink.ungrouped.push_str(§ion),
}
}
}
fn declaration_position<T>(item: &T, declared: &[T]) -> usize {
let offset = core::ptr::from_ref(item)
.addr()
.wrapping_sub(declared.as_ptr().addr());
let Some(index) = offset.checked_div(core::mem::size_of::<T>()) else {
return declared
.iter()
.position(|candidate| core::ptr::eq(candidate, item))
.unwrap_or(usize::MAX);
};
declared
.get(index)
.filter(|candidate| core::ptr::eq(*candidate, item))
.map_or(usize::MAX, |_| index)
}
fn order_args<'a>(items: &mut Vec<&'a ArgMeta<'a>>, declared: &'a [ArgMeta<'a>]) {
fn compare(a: &ArgMeta<'_>, b: &ArgMeta<'_>, declared: &[ArgMeta<'_>]) -> core::cmp::Ordering {
let key = |item: &ArgMeta<'_>| {
let position = declaration_position(item, declared);
(
item.display_order.map_or(position, |order| order as usize),
position,
)
};
key(a).cmp(&key(b))
}
sort_rows(items, &mut |a, b| compare(a, b, declared));
}
fn order_flags<'a>(items: &mut Vec<&'a FlagMeta<'a>>, declared: &'a [FlagMeta<'a>]) {
fn compare(
a: &FlagMeta<'_>,
b: &FlagMeta<'_>,
declared: &[FlagMeta<'_>],
) -> core::cmp::Ordering {
let key = |item: &FlagMeta<'_>| {
let position = declaration_position(item, declared);
(
item.extra
.display_order
.map_or(position, |order| order as usize),
position,
)
};
key(a).cmp(&key(b))
}
sort_rows(items, &mut |a, b| compare(a, b, declared));
}
fn order_commands(items: &mut Vec<&&CommandMeta<'_>>) {
fn compare(a: &CommandMeta<'_>, b: &CommandMeta<'_>) -> core::cmp::Ordering {
a.extra
.display_order
.unwrap_or(999)
.cmp(&b.extra.display_order.unwrap_or(999))
.then_with(|| a.cmd.name.cmp(b.cmd.name))
}
sort_rows(items, &mut |a, b| compare(a, b));
}
fn command_help_section<'a>(sub: &'a CommandMeta<'a>, default_title: &str) -> Option<&'a str> {
sub.extra
.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.extra.hidden_longs.contains(long))
.map(|long| format!("--{long}"))
.or_else(|| {
meta.flag
.shorts
.iter()
.find(|short| !meta.extra.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, examples: &[Example<'_>], long: bool, style: Style) {
if examples.is_empty() {
return;
}
write_heading(out, "Examples", style);
for (index, example) in examples.iter().enumerate() {
if index > 0 {
out.push('\n');
}
if let Some(header) = example.header {
let _ = writeln!(out, " {header}:");
}
if let Some(help) = example.help.filter(|_| long) {
let _ = writeln!(out, " {help}");
}
let _ = writeln!(out, " $ {}", example.code);
}
}
fn terminal_width(meta: &CommandMeta<'_>) -> usize {
if let Some(width) = meta.extra.term_width {
return if width == 0 {
usize::MAX
} else {
usize::from(width)
};
}
let detected = std::env::var("COLUMNS")
.ok()
.and_then(|value| value.parse().ok())
.filter(|columns| *columns > 0)
.or_else(probed_width)
.unwrap_or(80);
match meta.extra.max_term_width {
Some(0) | None => detected,
Some(max) => detected.min(usize::from(max)),
}
}
fn probed_width() -> Option<usize> {
if cfg!(test) {
return None;
}
crate::tty::columns()
}
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 {
with_default_command_help(
spec,
path,
chain,
true,
Style::PLAIN,
false,
long_help_with(spec, path, chain, false),
)
}
fn long_help_with(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> String {
assemble(
spec,
path,
chain,
&page_sections(
spec,
path,
chain,
true,
inherit_version_actions,
false,
Style::PLAIN,
),
Style::PLAIN,
)
}
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,
painted: &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, " {painted}");
return if block { block_indent } else { indent };
};
if block {
let _ = writeln!(out, " {painted}");
write_wrapped_indented(out, help, width, block_indent);
return block_indent;
}
if overflow {
let lines = wrap_at(help, inline_room, room);
let _ = writeln!(out, " {painted} {}", lines[0]);
for line in &lines[1..] {
if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, "{}{line}", " ".repeat(indent));
}
}
return indent;
}
out.push_str(" ");
out.push_str(painted);
for _ in 0..col.saturating_sub(usage.chars().count()) {
out.push(' ');
}
out.push_str(" ");
if fits(help, room) {
out.push_str(help);
out.push('\n');
return indent;
}
let lines = wrap(help, room);
out.push_str(&lines[0]);
out.push('\n');
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, row: &Row<'_>, layout: AnnotationLayout) {
let AnnotationLayout { indent, width } = layout;
let Row {
choices,
env,
env_fallback,
deprecated_env,
default,
..
} = *row;
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.extra.deprecated,
meta.extra.deprecated_warn_at,
meta.extra.deprecated_remove_at,
) {
write_wrapped_indented(out, &label, width, indent);
}
}
fn inline_environment_notes(fallbacks: &[&str], deprecated: &[&str]) -> Option<String> {
let mut notes = Vec::new();
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(" "))
}
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 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 inherited = if ancestors.is_empty() {
Vec::new()
} else {
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 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();
let key = |flag: &FlagMeta<'_>| {
let position = inherited_positions
.iter()
.position(|candidate| core::ptr::eq(*candidate, flag))
.unwrap_or(usize::MAX);
(
flag.extra
.display_order
.map_or(position, |order| order as usize),
position,
)
};
sort_rows(&mut inherited, &mut |a, b| key(a.0).cmp(&key(b.0)));
inherited
};
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 = page_sections(spec, &path, &chain, long, false, false, Style::PLAIN);
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, true,
))
}
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, true,
))
}
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,
default_subcommand_help: false,
multicall: false,
root: &root,
..*spec
};
Some(assembled_help(
&viewed, &path, &chain, long, style, true, 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,
default_subcommand_help: false,
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 {
__usage_advanced_help(true);
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,
false,
));
let current = *chain.last().expect("a recursive page has a command");
let mut children: Vec<_> = current.subcommands.iter().filter(|cmd| !cmd.hide).collect();
order_commands(&mut children);
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, default_visible_child, display_usage_masked, flag_usage, flat_commands,
inline_environment_notes, long_annotations, long_help, paint_synopsis, painted_prose,
render, render_styled, render_view_at_styled, styled_flag_usage, styled_inline, usage_line,
wrap, write_heading, AnnotationLayout, Palette, Row, Shown, Style,
};
use crate::spec::{
ArgMeta, ClauseMeta, CommandExtra, CommandMeta, Example, FlagExtra, FlagMeta, Spec,
ViewMeta,
};
use crate::{Arg, ArgAction, Clause, Command, Flag};
#[test]
fn declaration_positions_match_identity_search() {
let values = [11_u32, 22, 11, 44];
let other = 11;
for declared in [&values[..], &values[1..3], &values[..0]] {
for item in values.iter().chain([&other]) {
let expected = declared
.iter()
.position(|candidate| core::ptr::eq(candidate, item))
.unwrap_or(usize::MAX);
assert_eq!(super::declaration_position(item, declared), expected);
}
}
let values = [(); 3];
for declared in [&values[..], &values[..0]] {
for item in &values {
let expected = declared
.iter()
.position(|candidate| core::ptr::eq(candidate, item))
.unwrap_or(usize::MAX);
assert_eq!(super::declaration_position(item, declared), expected);
}
}
}
#[test]
fn filtered_help_rows_keep_declaration_order_as_the_tie_breaker() {
macro_rules! check {
($order:ident, $row:expr) => {{
let row = $row;
let declared = [
row(None),
row(Some(1)),
row(None),
row(Some(0)),
row(Some(1)),
];
let mut rows = vec![&declared[4], &declared[2], &declared[3], &declared[1]];
super::$order(&mut rows, &declared);
for (row, expected) in rows.iter().zip([3, 1, 4, 2]) {
assert!(core::ptr::eq(*row, &declared[expected]));
}
}};
}
check!(order_args, |display_order| ArgMeta {
display_order,
..ArgMeta::EMPTY
});
let extras = [None, Some(0), Some(1)].map(|display_order| FlagExtra {
display_order,
..FlagExtra::EMPTY
});
check!(order_flags, |display_order: Option<usize>| FlagMeta {
extra: &extras[display_order.map_or(0, |order| order + 1)],
..FlagMeta::EMPTY
});
}
#[test]
fn compiled_clause_arguments_appear_in_usage_and_help() {
let postinstall = Flag {
key: 4,
name: "postinstall",
longs: &["postinstall"],
..Flag::VALUE
};
let task = Arg {
name: "TASK",
..Arg::REQUIRED
};
let args = Arg {
name: "ARGS",
required: false,
var: true,
..Arg::REQUIRED
};
let command = Command {
name: "run",
flags: &[&postinstall],
clause: Some(&Clause {
key: 0,
name: "tasks",
separator: Some(b":::"),
flags: &[&postinstall],
args: &[&task, &args],
}),
..Command::EMPTY
};
let task_meta = ArgMeta {
arg: &task,
required: true,
help: Some("Task to run"),
..ArgMeta::EMPTY
};
let args_meta = ArgMeta {
arg: &args,
required: false,
var_min: Some(0),
help: Some("Arguments for the task"),
..ArgMeta::EMPTY
};
let meta = CommandMeta {
cmd: &command,
flags: &[FlagMeta {
flag: &postinstall,
required: true,
help: Some("Command to run after install"),
..FlagMeta::EMPTY
}],
extra: &CommandExtra {
clause: Some(&ClauseMeta {
name: "tasks",
separator: Some(":::"),
flags: &[FlagMeta {
flag: &postinstall,
required: true,
help: Some("Command to run after install"),
..FlagMeta::EMPTY
}],
help: None,
long_help: None,
canonical_selector: |_| None,
args: &[task_meta, args_meta],
}),
..CommandExtra::EMPTY
},
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
root: &meta,
..Spec::EMPTY
};
assert_eq!(
usage_line(&["ex"], &meta),
"ex [<TASK> [ARGS]… [::: <TASK> [ARGS]…]…]"
);
let help = super::short_help(&spec, &["ex"], &[&meta]);
assert!(help.contains("Arguments:"), "{help}");
assert!(help.contains("<TASK>"), "{help}");
assert!(help.contains("[ARGS]…"), "{help}");
}
#[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 root_help_stays_on_the_root_page() {
let leaf_cmd = Command {
name: "now",
..Command::EMPTY
};
let leaf_meta = CommandMeta {
cmd: &leaf_cmd,
about: Some("run it now"),
..CommandMeta::EMPTY
};
let sub_commands = [&leaf_cmd];
let sub_cmd = Command {
name: "run",
subcommands: &sub_commands,
..Command::EMPTY
};
let sub_subcommands = [&leaf_meta];
let sub_examples = [Example {
code: "ex run",
header: None,
help: None,
}];
let sub_meta = CommandMeta {
cmd: &sub_cmd,
about: Some("run it"),
subcommands: &sub_subcommands,
extra: &CommandExtra {
after_help: Some("run after"),
after_long_help: Some("run after long"),
examples: &sub_examples,
..CommandExtra::EMPTY
},
..CommandMeta::EMPTY
};
let root_commands = [&sub_cmd];
let root_cmd = Command {
name: "ex",
subcommands: &root_commands,
..Command::EMPTY
};
let root_subcommands = [&sub_meta];
let examples = [Example {
code: "ex build",
header: None,
help: None,
}];
let root_meta = CommandMeta {
cmd: &root_cmd,
subcommands: &root_subcommands,
extra: &CommandExtra {
before_help: Some("root before"),
before_long_help: Some("root before long"),
after_help: Some("root after"),
after_long_help: Some("root after long"),
examples: &examples,
..CommandExtra::EMPTY
},
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
author: Some("Root Author"),
license: Some("MIT"),
root: &root_meta,
..Spec::EMPTY
};
for page in [
super::short_help(
&spec,
&["ex", "run", "now"],
&[&root_meta, &sub_meta, &leaf_meta],
),
super::long_help(
&spec,
&["ex", "run", "now"],
&[&root_meta, &sub_meta, &leaf_meta],
),
] {
for root_only in [
"root before",
"root after",
"$ ex build",
"run after",
"$ ex run",
"Root Author",
"License: MIT",
] {
assert!(
!page.contains(root_only),
"inherited ancestor metadata {root_only:?}:\n{page}"
);
}
}
let sub_short = super::short_help(&spec, &["ex", "run"], &[&root_meta, &sub_meta]);
let sub_long = super::long_help(&spec, &["ex", "run"], &[&root_meta, &sub_meta]);
assert!(sub_short.contains("run after"), "{sub_short}");
assert!(sub_short.contains("$ ex run"), "{sub_short}");
assert!(sub_long.contains("run after long"), "{sub_long}");
assert!(sub_long.contains("$ ex run"), "{sub_long}");
let short = super::short_help(&spec, &["ex"], &[&root_meta]);
let long = super::long_help(&spec, &["ex"], &[&root_meta]);
assert!(short.contains("root before"), "{short}");
assert!(short.contains("root after"), "{short}");
assert!(short.contains("$ ex build"), "{short}");
assert!(long.contains("root before long"), "{long}");
assert!(long.contains("root after long"), "{long}");
assert!(long.contains("$ ex build"), "{long}");
assert!(long.contains("Author: Root Author"), "{long}");
assert!(long.contains("License: MIT"), "{long}");
}
#[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"),
extra: &FlagExtra {
deprecated: Some("use --new"),
..FlagExtra::EMPTY
},
..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 {
subcommands: &subcommands,
extra: &CommandExtra {
next_line_help: true,
..CommandExtra::EMPTY
},
..CommandMeta::EMPTY
};
let mut page = String::new();
flat_commands(&mut page, &["tool"], &root_meta, 80, false, Style::PLAIN);
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,
extra: &FlagExtra {
deprecated: Some("use --new"),
..FlagExtra::EMPTY
},
..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 {
subcommands: &subcommands,
extra: &CommandExtra {
next_line_help: true,
..CommandExtra::EMPTY
},
..CommandMeta::EMPTY
};
let mut page = String::new();
flat_commands(&mut page, &["tool"], &root_meta, 80, false, Style::PLAIN);
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,
extra: &FlagExtra {
env_fallback: &["OLD_TOKEN"],
deprecated_env: &["LEGACY_TOKEN"],
..FlagExtra::EMPTY
},
..FlagMeta::EMPTY
};
let row = Row::flag(&meta, "--token");
let mut page = String::new();
long_annotations(
&mut page,
&row,
AnnotationLayout {
indent: 4,
width: 80,
},
);
assert!(page.is_empty());
assert!(inline_environment_notes(row.env_fallback, row.deprecated_env).is_none());
let visible = inline_environment_notes(&["OLD_TOKEN"], &["LEGACY_TOKEN"])
.expect("visible environment notes");
assert!(visible.contains("[env fallback: OLD_TOKEN]"));
assert!(visible.contains("[deprecated env: LEGACY_TOKEN]"));
}
#[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, None, Style::PLAIN);
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,
extra: &CommandExtra {
after_help: Some("More help.\n"),
..CommandExtra::EMPTY
},
..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_only_the_structure_it_wrote() {
let prose = "A summary ending in:\nUsage: prose is not a synopsis\nOptions:\n -f, --force Only mentioned\n build Also only mentioned\n";
let page = |style: Style| {
let mut page = prose.to_string();
let mut usage = "Usage: ex [OPTIONS]\n ex --all\n".to_string();
paint_synopsis(&mut usage, style);
page.push_str(&usage);
write_heading(&mut page, "Options", style);
page.push_str(" [possible values: --auto]\n (default: -1)\n");
painted_prose(page, style)
};
let plain = page(Style::PLAIN);
assert_eq!(
plain,
format!("{prose}Usage: ex [OPTIONS]\n ex --all\n\nOptions:\n [possible values: --auto]\n (default: -1)\n")
);
let coloured = page(Style::COLOURED);
assert!(coloured.starts_with(prose), "{coloured:?}");
assert!(coloured.contains("\u{1b}[1;33mUsage:\u{1b}[0m ex [\u{1b}[1;35mOPTIONS\u{1b}[0m]"));
assert!(coloured.contains("\n ex \u{1b}[1;32m--all\u{1b}[0m\n"));
assert!(coloured.contains("\n\u{1b}[1;33mOptions:\u{1b}[0m\n"));
assert!(coloured.contains("[possible values: --auto]"));
assert!(!coloured.contains('\u{1}'), "{coloured:?}");
assert_eq!(strip_ansi(&coloured), plain);
}
#[test]
fn rendered_command_rows_receive_command_style() {
let build_command = Command {
name: "build",
..Command::EMPTY
};
let root_command = Command {
name: "ex",
subcommands: &[&build_command],
..Command::EMPTY
};
let build_meta = CommandMeta {
cmd: &build_command,
about: Some("Build it"),
..CommandMeta::EMPTY
};
let root_meta = CommandMeta {
cmd: &root_command,
subcommands: &[&build_meta],
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
root: &root_meta,
..Spec::EMPTY
};
let plain = render_styled(&spec, &root_command, false, Style::PLAIN).expect("root help");
let coloured =
render_styled(&spec, &root_command, false, Style::COLOURED).expect("root help");
assert!(
coloured.contains("\u{1b}[1;32mbuild\u{1b}[0m Build it"),
"{coloured:?}"
);
assert!(
coloured.contains(
"\u{1b}[1;32mhelp\u{1b}[0m Print this message or the help of the given subcommand(s)"
),
"{coloured:?}"
);
assert_eq!(strip_ansi(&coloured), plain);
}
#[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 plain_help_strips_ansi_authored_before_style_selection() {
let command = Command {
name: "ex",
..Command::EMPTY
};
let root = CommandMeta {
cmd: &command,
extra: &CommandExtra {
after_long_help: Some(
"\u{1b}[1m\u{1b}[4mExamples:\u{1b}[22m\u{1b}[24m\n\n \u{1b}[1mex run\u{1b}[22m",
),
..CommandExtra::EMPTY
},
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
root: &root,
..Spec::EMPTY
};
let plain = render_styled(&spec, &command, true, Style::PLAIN).expect("root page");
assert!(plain.contains("Examples:\n\n ex run"), "{plain}");
assert!(!plain.contains('\u{1b}'), "{plain:?}");
assert_eq!(render(&spec, &command, true).as_deref(), Some(&*plain));
let coloured = render_styled(&spec, &command, true, Style::COLOURED).expect("root page");
assert!(coloured.contains("\u{1b}[1m\u{1b}[4mExamples:"));
assert_eq!(strip_ansi(&coloured), plain);
}
#[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 a_palette_remaps_metavar_colour_without_changing_plain_text() {
let cyan = Style::COLOURED.palette(Palette::DEFAULT.metavar("cyan+bold"));
assert_eq!(
styled_flag_usage("--output=<FILE>", cyan),
"\u{1b}[1;32m--output\u{1b}[0m=\u{1b}[1;36m<FILE>\u{1b}[0m"
);
assert_eq!(
styled_flag_usage(
"--output=<FILE>",
Style::PLAIN.palette(Palette::DEFAULT.metavar("cyan+bold"))
),
"--output=<FILE>"
);
}
#[test]
fn a_palette_remaps_template_role_tags() {
let command = Command {
name: "ex",
..Command::EMPTY
};
let root = CommandMeta {
cmd: &command,
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
help_template: Some("{$heading}CUSTOM HELP{/$}\n\n{{usage}}"),
root: &root,
..Spec::EMPTY
};
let cyan = Style::COLOURED.palette(Palette::DEFAULT.heading("cyan+bold"));
let coloured = render_styled(&spec, &command, false, cyan).expect("root page");
assert!(
coloured.starts_with("\u{1b}[1;36mCUSTOM HELP\u{1b}[0m"),
"{coloured:?}"
);
}
#[test]
fn a_palette_expands_role_names_once() {
let palette = Palette::DEFAULT.heading("cyan+bold").metavar("heading");
assert_eq!(
styled_flag_usage("<FILE>", Style::COLOURED.palette(palette)),
"\u{1b}[1;33m<FILE>\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,
subcommands: &[&run_meta],
extra: &CommandExtra {
flatten_help: true,
..CommandExtra::EMPTY
},
..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 = painted_prose(page.to_string(), 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
}
#[test]
fn a_commands_own_name_outranks_another_commands_alias() {
let query = Command {
name: "query",
aliases: &["install"],
..Command::EMPTY
};
let install = Command {
name: "install",
..Command::EMPTY
};
let query_meta = CommandMeta {
cmd: &query,
..CommandMeta::EMPTY
};
let install_meta = CommandMeta {
cmd: &install,
..CommandMeta::EMPTY
};
let root_cmd = Command {
name: "ex",
..Command::EMPTY
};
let root_meta = CommandMeta {
cmd: &root_cmd,
subcommands: &[&query_meta, &install_meta],
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
default_subcommand: Some("install"),
root: &root_meta,
..Spec::EMPTY
};
let resolved = default_visible_child(&spec, &root_meta).expect("a default is resolved");
assert_eq!(resolved.cmd.name, "install");
}
}
#[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",
"--long",
Some("alpha beta"),
col,
width,
false,
);
assert_eq!(page, " --long\n alpha\n beta\n");
}