use std::borrow::Cow;
use crate::Command;
use crate::DoubleDash;
use crate::order::sort_by as sort_rows;
use crate::spec::{
AdmonitionKind, AdmonitionMeta, ArgMeta, CommandMeta, Example, FlagMeta, Spec, ViewMeta,
};
use crate::text::char_count;
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) = find_str(rest, "{{") {
let after = tail(rest, at + 2);
let Some(end) = find_str(after, "}}") else {
return Err("a `{{` with no `}}` after it");
};
let name = head(after, end).trimmed();
if !SECTIONS.contains(&name) {
return Ok(Some(name));
}
rest = tail(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.put_str(part);
}
out
}
fn named(&self, name: &str) -> Option<String> {
let part = match name {
"about" => &self.about,
"usage" => &self.usage,
"commands" => {
let mut out = self.commands.trimmed().to_string();
let flattened = self.flattened.trimmed();
if !flattened.is_empty() {
if !out.is_empty() {
out.put_str("\n\n");
}
out.put_str(flattened);
}
return Some(out);
}
"args" => &self.args,
"flags" => &self.flags,
"grouped_args" => &self.grouped_args,
"ungrouped_args" => &self.ungrouped_args,
"grouped_flags" => &self.grouped_flags,
"ungrouped_flags" => &self.ungrouped_flags,
"after_help" => &self.after_help,
_ => return None,
};
Some(part.trimmed().to_string())
}
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.is_blank()) {
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 page_lines = lines_of(page);
let column = reserved.filter(|column| {
page_lines
.iter()
.take(art.len())
.all(|line| shown_width(line) + LOGO_GUTTER <= *column)
});
let mut out = match column {
Some(column) => {
let mut out = String::with_capacity(page.len() + art.len() * column);
let mut text = page_lines.iter().copied();
for index in 0..page_lines.len().max(art.len()) {
if index > 0 {
out.put_char('\n');
}
let line = text.next().unwrap_or("");
out.put_str(line);
if let Some(art) = art.get(index).filter(|art| !art.is_empty()) {
for _ in 0..column.saturating_sub(shown_width(line)) {
out.put_char(' ');
}
out.put_str(&painted_logo(art, style, coloured));
}
}
out
}
None if art_width <= width => {
let mut banner = String::new();
for art in &art {
banner.put_str(&painted_logo(art, style, coloured));
banner.put_char('\n');
}
banner.put_char('\n');
banner.put_str(page);
banner
}
None => return page.to_string(),
};
out.truncate(out.trimmed_end().len());
out.put_char('\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.is_blank() => template::semantic(style, line, Style::COLOURED),
_ => line.to_string(),
}
}
fn logo_lines(logo: &str) -> Vec<&str> {
let mut lines = lines_of(logo);
for line in &mut lines {
*line = line.trimmed_end();
}
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 byte_at(bytes, at) == b'\x1b' && byte_at(bytes, at + 1) == b'[' {
let mut end = at + 2;
while end < bytes.len() && !(0x40..=0x7e).contains(&byte_at(bytes, end)) {
end += 1;
}
at = (end + 1).min(bytes.len());
continue;
}
if byte_at(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.trimmed();
let mut done = String::with_capacity(trimmed.len() + 1);
done.put_str(trimmed);
done.put_char('\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 byte_at(bytes, at) != b'\x1b' || byte_at(bytes, at + 1) != b'[' {
at += 1;
continue;
}
let mut end = at + 2;
while end < bytes.len() && (0x30..=0x3f).contains(&byte_at(bytes, end)) {
end += 1;
}
while end < bytes.len() && (0x20..=0x2f).contains(&byte_at(bytes, end)) {
end += 1;
}
if end == bytes.len() || !(0x40..=0x7e).contains(&byte_at(bytes, end)) {
at += 2;
continue;
}
plain.put_str(slice(&text, copied, at));
copied = end + 1;
at = copied;
}
plain.put_str(tail(&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())
}
#[cfg(feature = "diagnostics")]
pub(crate) const fn is_coloured(self) -> bool {
self.coloured
}
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 paint(self, out: &mut String, role: &str, text: &str) {
template::semantic_into(out, role, text, self);
}
fn heading(self, text: &str) -> String {
template::semantic("heading", 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)
}
}
#[inline(never)]
fn span_kind(kind: usize) -> Option<(&'static str, &'static str, &'static str, bool, bool)> {
Some(match kind {
0 => ("***", "1;3", "22;23", false, true),
1 => ("___", "1;3", "22;23", true, true),
2 => ("**", "1", "22", false, true),
3 => ("__", "1", "22", true, true),
4 => ("~~", "9", "29", false, true),
5 => ("*", "3", "23", false, true),
6 => ("_", "3", "23", true, true),
7 => ("`", "36", "39", false, false),
_ => return 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 = tail(text, at);
if let Some(escaped) = rest.strip_prefix('\\').and_then(|after| after.first_char())
&& matches!(escaped, '*' | '_' | '~' | '`' | '\\')
{
out.put_char(escaped);
at += 1 + escaped.len_utf8();
allow_run_remainder = false;
continue;
}
let span = (0..8).find_map(|kind| {
let (delimiter, open, close, word_boundary, recurse) = span_kind(kind)?;
rest.strip_prefix(delimiter)?;
let marker = delimiter.first_char().unwrap_or_default();
let previous = head(text, at).last_char();
if (previous == Some(marker) && !allow_run_remainder)
|| (delimiter.len() == 1 && tail(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.put_str("\u{1b}[");
out.put_str(open);
out.put_char('m');
if recurse {
out.put_str(&styled_inline(slice(text, content_start, end), Some(open)));
} else {
out.put_str(slice(text, content_start, end));
}
out.put_str("\u{1b}[");
out.put_str(close);
out.put_char('m');
if let Some(parent) = parent {
out.put_str("\u{1b}[");
out.put_str(parent);
out.put_char('m');
}
let marker = delimiter.first_char().unwrap_or_default();
allow_run_remainder =
tail(text, after).starts_with(marker) && head(text, after).ends_with(marker);
at = after;
continue;
}
let ch = rest.first_char().unwrap_or_default();
out.put_char(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.first_char()?;
let marker_byte = byte_at(delimiter.as_bytes(), 0);
let bytes = text.as_bytes();
let width = delimiter.len();
let mut search_at = content_start;
while let Some(found) = find_char(tail(text, search_at), marker) {
let run_start = search_at + found;
let mut run_len = 0;
while byte_at(bytes, run_start + run_len) == marker_byte {
run_len += 1;
}
let run_end = run_start + run_len;
let mut backslashes = 0;
while backslashes < run_start && byte_at(bytes, run_start - 1 - backslashes) == b'\\' {
backslashes += 1;
}
let escaped = backslashes % 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 = slice(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
|| !tail(text, after)
.chars()
.next()
.is_some_and(char::is_alphanumeric);
if run_start > content_start
&& !slice(text, content_start, run_start).is_blank()
&& 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 = tail(usage, at);
let previous = head(usage, at).last_char();
if rest.starts_with('-')
&& previous.is_none_or(|c| is_space(c) || matches!(c, ',' | ':' | '[' | '<'))
{
let end = rest
.char_indices()
.skip(1)
.find_map(|(i, c)| {
(is_space(c) || matches!(c, ',' | '=' | '[' | ']' | '<' | '>')).then_some(i)
})
.unwrap_or(rest.len());
style.paint(&mut out, "option", head(rest, end));
at += end;
continue;
}
if rest.starts_with("<-") {
out.put_char('<');
at += 1;
continue;
}
if rest.starts_with('<')
&& let Some(end) = find_char(rest, '>')
{
let end = end + 1;
style.paint(&mut out, "metavar", head(rest, end));
at += end;
continue;
}
if let Some(value) = rest.strip_prefix("[=")
&& let Some(end) = find_char(value, ']')
{
out.put_str("[=");
style.paint(&mut out, "metavar", head(value, end));
out.put_char(']');
at += end + 3;
continue;
}
if let Some(value) = rest.strip_prefix('=') {
out.put_char('=');
at += 1;
if !value.starts_with('<') {
let end = value
.find(|c: char| is_space(c) || matches!(c, ',' | ']' | '>'))
.unwrap_or(value.len());
if end > 0 {
style.paint(&mut out, "metavar", head(value, end));
at += end;
}
}
continue;
}
if previous == Some('[') && !rest.starts_with('-') {
let end = find_char(rest, ']').unwrap_or(rest.len());
if end > 0 {
style.paint(&mut out, "metavar", head(rest, end));
at += end;
continue;
}
}
if rest.starts_with(|c: char| c.is_ascii_uppercase())
&& previous.is_none_or(|c| is_space(c) || matches!(c, '=' | '[' | '<'))
{
let end = rest
.find(|c: char| {
!(c.is_ascii_uppercase() || c.is_ascii_digit() || matches!(c, '_' | '-' | '@'))
})
.unwrap_or(rest.len());
let boundary = tail(rest, end).first_char();
if boundary
.is_none_or(|c| is_space(c) || matches!(c, ',' | '=' | '[' | ']' | '<' | '>' | '.'))
{
style.paint(&mut out, "metavar", head(rest, end));
at += end;
continue;
}
}
let ch = rest.first_char().unwrap_or_default();
out.put_char(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}';
trait PutStr {
fn put_str(&mut self, text: &str);
fn put_char(&mut self, c: char);
}
impl PutStr for String {
#[inline(never)]
fn put_str(&mut self, text: &str) {
self.push_str(text);
}
#[inline(never)]
fn put_char(&mut self, c: char) {
self.push(c);
}
}
trait Text {
fn trimmed(&self) -> &str;
fn trimmed_start(&self) -> &str;
fn trimmed_end(&self) -> &str;
fn is_blank(&self) -> bool;
fn first_char(&self) -> Option<char>;
fn last_char(&self) -> Option<char>;
}
impl Text for str {
#[inline(never)]
fn trimmed(&self) -> &str {
self.trim()
}
#[inline(never)]
fn trimmed_start(&self) -> &str {
self.trim_start()
}
#[inline(never)]
fn trimmed_end(&self) -> &str {
self.trim_end()
}
#[inline(never)]
fn is_blank(&self) -> bool {
self.trim().is_empty()
}
#[inline(never)]
fn first_char(&self) -> Option<char> {
self.chars().next()
}
#[inline(never)]
fn last_char(&self) -> Option<char> {
self.chars().next_back()
}
}
#[inline(never)]
fn is_space(c: char) -> bool {
c.is_whitespace()
}
#[inline(never)]
fn put(out: &mut String, parts: &[&str]) {
for part in parts {
out.put_str(part);
}
}
#[inline(never)]
fn joined(parts: &[&str], separator: &str) -> String {
let mut out = String::new();
for (index, part) in parts.iter().enumerate() {
if index > 0 {
out.put_str(separator);
}
out.put_str(part);
}
out
}
#[inline(never)]
fn cat(parts: &[&str]) -> String {
let mut out = String::new();
put(&mut out, parts);
out
}
#[inline(never)]
fn find_char(text: &str, c: char) -> Option<usize> {
let mut buf = [0u8; 4];
find_str(text, c.encode_utf8(&mut buf))
}
#[inline(never)]
fn find_str(text: &str, needle: &str) -> Option<usize> {
let needle = needle.as_bytes();
let Some(&first) = needle.first() else {
return Some(0);
};
let bytes = text.as_bytes();
let mut from = 0;
loop {
let at = from + find_byte(bytes.get(from..)?, first)?;
if bytes.get(at..).is_some_and(|rest| rest.starts_with(needle)) {
return Some(at);
}
from = at + 1;
}
}
fn find_byte(bytes: &[u8], wanted: u8) -> Option<usize> {
const LOW: u64 = 0x0101_0101_0101_0101;
const HIGH: u64 = 0x8080_8080_8080_8080;
let pattern = LOW * u64::from(wanted);
let (words, rest) = bytes.as_chunks::<8>();
let mut base = 0;
for chunk in words {
let word = u64::from_le_bytes(*chunk) ^ pattern;
if word.wrapping_sub(LOW) & !word & HIGH != 0 {
return chunk
.iter()
.position(|&byte| byte == wanted)
.map(|at| base + at);
}
base += 8;
}
rest.iter()
.position(|&byte| byte == wanted)
.map(|at| base + at)
}
#[inline(never)]
fn split_first(text: &str, separator: char) -> (&str, Option<&str>) {
let mut buf = [0u8; 4];
let separator = separator.encode_utf8(&mut buf);
match find_str(text, separator) {
Some(at) => (head(text, at), Some(tail(text, at + separator.len()))),
None => (text, None),
}
}
#[inline(always)]
fn split_line(text: &str) -> (&str, Option<&str>) {
split_first(text, '\n')
}
#[inline(never)]
fn lines_of(text: &str) -> Vec<&str> {
text.lines().collect()
}
fn without_binary<'a>(path: &'a [&'a str]) -> &'a [&'a str] {
path.get(1..).unwrap_or_default()
}
fn byte_at(bytes: &[u8], at: usize) -> u8 {
bytes.get(at).copied().unwrap_or(0)
}
#[inline(never)]
fn tail(text: &str, at: usize) -> &str {
&text[at..]
}
#[inline(never)]
fn head(text: &str, at: usize) -> &str {
&text[..at]
}
#[inline(never)]
fn slice(text: &str, from: usize, to: usize) -> &str {
&text[from..to]
}
#[inline(never)]
fn put_pad(out: &mut String, indent: usize) {
for _ in 0..indent {
out.put_char(' ');
}
}
#[inline(never)]
fn line_at(out: &mut String, indent: usize, text: &str) {
put_pad(out, indent);
out.put_str(text);
out.put_char('\n');
}
#[inline(never)]
fn put_bracketed(out: &mut String, open: char, prefix: &str, name: &str, close: char) {
out.put_char(open);
put(out, &[prefix, name]);
out.put_char(close);
}
#[inline(never)]
fn long_form(name: &str) -> String {
cat(&["--", name])
}
#[inline(never)]
fn short_form(short: u8) -> String {
let mut out = String::from("-");
out.put_char(short as char);
out
}
#[inline(never)]
fn write_heading(out: &mut String, title: &str, style: Style) {
out.put_char('\n');
if style.coloured {
out.put_char(STRUCTURE);
let mut titled = String::from(title);
titled.put_char(':');
out.put_str(&style.heading(&titled));
} else {
out.put_str(title);
out.put_char(':');
}
out.put_char('\n');
}
#[inline(never)]
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);
let mut rest = usage.as_str();
while !rest.is_empty() {
let (body, next) = split_line(rest);
let newline = if next.is_some() { "\n" } else { "" };
rest = next.unwrap_or("");
out.put_char(STRUCTURE);
match body.strip_prefix("Usage:") {
Some(rest) => {
out.put_str(&style.heading("Usage:"));
out.put_str(&styled_flag_usage(rest, style));
}
None => out.put_str(&styled_flag_usage(body, style)),
}
out.put_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());
let mut rest = page.as_str();
while !rest.is_empty() {
let (body, next) = split_line(rest);
let newline = if next.is_some() { "\n" } else { "" };
rest = next.unwrap_or("");
if let Some(structure) = body.strip_prefix(STRUCTURE) {
out.put_str(structure);
} else if body.trimmed_start().starts_with("$ ") {
out.put_str(body);
} else {
out.put_str(&style.inline(body));
}
out.put_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.is_blank()) {
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.put_char(' ');
}
out.put_str(part);
}
let listed = |f: &&FlagMeta<'_>| !f.hide && !f.builtin && !is_clause_flag(meta, f);
let mut flags = 0;
let mut required = false;
for flag in meta.flags.iter().filter(listed) {
flags += 1;
required |= flag_demanded(flag);
}
if flags > 0 {
if flags <= INLINE_LIMIT {
for flag in meta.flags.iter().filter(listed) {
let (open, close) = if flag_demanded(flag) {
('<', '>')
} else {
('[', ']')
};
out.put_char(' ');
out.put_char(open);
out.put_str(&flag_usage(flag));
out.put_char(close);
}
} else if required {
out.put_str(" <FLAGS>");
} else {
out.put_str(" [FLAGS]");
}
}
let positional_args = positional_args(meta);
let mut args = 0;
let mut required = false;
for arg in positional_args.iter().filter(|a| !a.hide) {
args += 1;
required |= demanded(arg);
}
if args > 0 {
if let Some(clause) = meta.extra.clause {
let mut inner = String::new();
for (index, arg) in positional_args.iter().filter(|a| !a.hide).enumerate() {
if index > 0 {
inner.put_char(' ');
}
inner.put_str(&arg_usage(arg));
}
match clause.separator {
Some(separator) => {
put(
&mut out,
&[" [", &inner, " [", separator, " ", &inner, "]…]"],
);
}
None => {
if let Some(arg) = positional_args.iter().find(|arg| !arg.hide) {
let separator = if arg.arg.double_dash == DoubleDash::Required {
"-- "
} else {
""
};
put(&mut out, &[" [", separator, arg.arg.name, "]…"]);
}
}
}
} else if args <= INLINE_LIMIT {
for arg in positional_args.iter().filter(|a| !a.hide) {
out.put_char(' ');
out.put_str(&arg_usage(arg));
}
} else if required {
out.put_str(" <ARGS>…");
} else {
out.put_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 {
put(&mut out, &[" <", name, ">"]);
} else {
put(&mut out, &[" [", name, "]"]);
}
}
out
}
fn usage_section(out: &mut String, spec: &Spec<'_>, path: &[&str], meta: &CommandMeta<'_>) {
if path.len() <= 1
&& let Some(usage) = spec.usage.filter(|usage| !usage.is_blank())
{
put(out, &[usage.trimmed(), "\n"]);
return;
}
if !flatten_help(meta) || !meta.subcommands.iter().any(|sub| !sub.hide) {
put(out, &["Usage: ", &usage_line(path, meta), "\n"]);
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() {
put(out, &["Usage: ", first, "\n"]);
for line in rest {
put(out, &[" ", line, "\n"]);
}
}
}
fn flag_usage(meta: &FlagMeta<'_>) -> String {
flag_usage_masked(meta, &Shown::all(meta))
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Form<'a> {
Long(&'a str),
Short(u8),
}
#[inline(never)]
pub(crate) fn push_forms<'a>(out: &mut Vec<Form<'a>>, f: &'a FlagMeta<'a>) {
for long in f.flag.longs {
out.push(Form::Long(long));
}
for short in f.flag.shorts {
out.push(Form::Short(*short));
}
}
#[inline(never)]
fn has_form(forms: &[Form<'_>], form: Form<'_>) -> bool {
forms.contains(&form)
}
#[inline(never)]
fn has_name(names: &[&str], name: &str) -> bool {
for candidate in names {
if *candidate == name {
return true;
}
}
false
}
#[inline(never)]
fn first_long<'a>(longs: &[&'a str], keep: &dyn Fn(&str) -> bool) -> Option<&'a str> {
longs.iter().copied().find(|long| keep(long))
}
#[inline(never)]
fn first_short(shorts: &[u8], keep: &dyn Fn(u8) -> bool) -> Option<u8> {
shorts.iter().copied().find(|short| keep(*short))
}
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: first_long(meta.flag.longs, &|long| {
!has_name(meta.extra.hidden_longs, long)
}),
short: first_short(meta.flag.shorts, &|short| {
!meta.extra.hidden_shorts.contains(&short)
}),
negate: meta.flag.negate.is_some(),
}
}
fn surviving(
meta: &'a FlagMeta<'a>,
taken: &[Form<'_>],
taken_negations: &[Form<'_>],
every_form: &[Form<'_>],
) -> Self {
Shown {
long: first_long(meta.flag.longs, &|l| {
!has_name(meta.extra.hidden_longs, l) && !has_form(taken, Form::Long(l))
}),
short: first_short(meta.flag.shorts, &|s| {
!meta.extra.hidden_shorts.contains(&s) && !has_form(taken, Form::Short(s))
}),
negate: meta.flag.negate.is_some_and(|n| {
let spelling = Form::Long(n);
!has_form(taken_negations, spelling)
&& (!has_form(every_form, spelling) || has_name(meta.flag.longs, n))
}),
}
}
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 {
put(&mut out, &[flag.name, ":"]);
}
if let Some(short) = short {
if !out.is_empty() {
out.put_char(' ');
}
out.put_char('-');
out.put_char(*short as char);
}
if let Some(long) = long {
if !out.is_empty() {
out.put_char(' ');
}
put(&mut out, &["--", long]);
}
if flag.takes_value {
let exact = exact_arity(meta.extra.value_var_min, meta.extra.value_var_max);
let names = meta.extra.value_names;
let single = names.len() <= 1;
let count = if !single {
names.len()
} else {
exact.filter(|n| *n > 1).unwrap_or(1)
};
let only = names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
for index in 0..count {
append_flag_value(
&mut out,
names.get(index).copied().unwrap_or(only),
meta.value_optional,
flag.require_equals,
index == 0,
);
}
if flag.variadic && meta.extra.value_names.len() <= 1 && exact.is_none() {
out.put_char('…');
}
}
out
}
fn append_flag_value(
out: &mut String,
name: &str,
optional: bool,
require_equals: bool,
first: bool,
) {
if first && optional && require_equals {
put(out, &["[=", name, "]"]);
} else {
let separator = if first && require_equals { "=" } else { " " };
let (open, close) = if optional { ('[', ']') } else { ('<', '>') };
out.put_str(separator);
out.put_char(open);
out.put_str(name);
out.put_char(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);
let single = meta.value_names.len() <= 1;
let count = if !single {
meta.value_names.len()
} else {
exact.filter(|n| *n > 1).unwrap_or(1)
};
let separator = arg.double_dash == DoubleDash::Required;
if !single && separator {
out.put_str("-- ");
}
for index in 0..count {
if index > 0 {
out.put_char(' ');
}
let (name, prefix) = if single {
(arg.name, if separator && count == 1 { "-- " } else { "" })
} else {
(meta.value_names.get(index).copied().unwrap_or_default(), "")
};
put_bracketed(&mut out, open, prefix, name, close);
}
if arg.var && meta.value_names.len() <= 1 && exact.is_none() {
out.put_char('…');
}
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 Some(&meta) = chain.last() else {
return Sections::default();
};
let (mut own, mut inherited) = own_and_global(chain, inherit_version_actions);
own.retain(|flag| !hidden_on(long, flag.hide_short_help, flag.hide_long_help));
if suppress_global {
inherited.clear();
} else {
inherited.retain(|(flag, _)| !hidden_on(long, flag.hide_short_help, flag.hide_long_help));
}
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.put_char('\n');
}
let root = path.len() <= 1;
if root && let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
put(out, &[name, " ", version, "\n"]);
}
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.trimmed_end(), width, 0);
out.put_char('\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,
without_binary(path),
meta,
width,
long,
root_default_command_name(spec, path, meta),
style,
);
}
let positional_args = positional_args(meta);
let mut shown_args: Vec<&ArgMeta<'_>> = Vec::new();
for arg in positional_args {
if !arg.hide && !hidden_on(long, arg.hide_short_help, arg.hide_long_help) {
shown_args.push(arg);
}
}
order_args(&mut shown_args, positional_args);
let mut args: Vec<(&ArgMeta<'_>, String)> = Vec::new();
let mut arg_longest = None;
for arg in shown_args {
let usage = arg_usage(arg);
arg_longest = arg_longest.max(Some(char_count(&usage)));
args.push((arg, usage));
}
let mut flag_longest = None;
let mut own_rows: Vec<(&FlagMeta<'_>, String)> = Vec::new();
for flag in own {
let usage = column_usage(flag);
flag_longest = flag_longest.max(Some(char_count(&usage)));
own_rows.push((flag, usage));
}
for (_, usage) in &inherited {
flag_longest = flag_longest.max(Some(char_count(usage)));
}
let own = own_rows;
let arg_col = arg_longest.map_or(0, |longest| usage_column_width(longest, width));
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 = flag_longest.map_or(0, |longest| usage_column_width(longest, width));
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,
without_binary(path),
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.put_char('\n');
write_wrapped_indented(out, after, width, 0);
}
if long && root && (spec.author.is_some() || spec.license.is_some()) {
out.put_char('\n');
if let Some(author) = spec.author {
put(out, &["Author: ", author, "\n"]);
}
if let Some(license) = spec.license {
put(out, &["License: ", license, "\n"]);
}
}
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 {
line_at(out, 2, &painted);
if let Some(help) = row.help.filter(|h| !h.is_blank()) {
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.trimmed_end().to_string();
out.put_str("\n\nDefault command: ");
out.put_str(&style.command(child.cmd.name));
out.put_str("\n Unmatched words select this command.\n\n");
out.put_str(child_page.trimmed_end());
out.put_char('\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<'_>> = Vec::new();
for sub in meta.subcommands {
if !sub.hide {
visible.push(sub);
}
}
order_commands(&mut visible);
if visible.is_empty() {
return;
}
let mut sub_path: Vec<&str> = path.to_vec();
sub_path.push("");
let last = sub_path.len() - 1;
let mut lines: Vec<(String, &&CommandMeta<'_>)> = Vec::new();
for sub in visible {
if let Some(name) = sub_path.get_mut(last) {
*name = sub.cmd.name;
}
lines.push((usage_line(&sub_path, sub), sub));
}
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 mut longest = if show_help {
char_count(HELP_SUBCOMMAND)
} else {
0
};
for (_, sub) in &lines {
longest = longest.max(char_count(sub.cmd.name));
}
let col = if lines.is_empty() && !show_help {
0
} else {
usage_column_width(longest, width)
};
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 && let Some(prose) = heading.and_then(|title| heading_help(meta, title)) {
write_wrapped_indented(out, prose, width, 2);
out.put_char('\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.put_str(summary);
}
if visible_aliases.peek().is_some() {
if !row.is_empty() {
row.put_char(' ');
}
row.put_str("[aliases: ");
for (index, alias) in visible_aliases.enumerate() {
if index > 0 {
row.put_str(", ");
}
row.put_str(alias);
}
row.put_char(']');
}
if let Some(label) = label {
if !row.is_empty() {
row.put_char(' ');
}
row.put_str(&label);
}
if is_default {
if !row.is_empty() {
row.put_char(' ');
}
row.put_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, &joined(&sub_path, " "), style);
let about = if long {
sub.long_about.or(sub.about)
} else {
sub.about
};
if let Some(about) = about.filter(|about| !about.is_blank()) {
write_wrapped_indented(out, about.trimmed_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| char_count(&arg_usage(arg)))
.max()
.map(|longest| usage_column_width(longest, width))
.unwrap_or(0);
let flag_col = flags
.iter()
.map(|flag| char_count(&column_usage(flag)))
.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.put_char('\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| items.get(index).and_then(&heading_of),
&prose_of,
&mut |out, index| {
if let Some(item) = items.get(index) {
write_item(out, item);
}
},
);
}
#[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) {
if let Some(upto) = headings.get_mut(..=index) {
upto.rotate_right(1);
}
}
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.put_char('\n');
}
for index in (0..len).filter(|&index| heading_of(index) == heading) {
write_item(&mut section, index);
}
sink.page.put_str(§ion);
match heading {
Some(_) => sink.grouped.put_str(§ion),
None => sink.ungrouped.put_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.put_char(' ');
}
out.put_str(part);
};
if !choices.is_empty() {
push(&cat(&["[", &joined(choices, ", "), "]"]));
}
if let Some(env) = env {
push(&cat(&["[env: ", env, "]"]));
}
if let Some(environment) = environment {
push(environment);
}
if !default.is_empty() {
push(&cat(&["(default: ", &joined(default, ", "), ")"]));
}
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(cat(&[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| long_form(long))
.or_else(|| {
meta.flag
.shorts
.iter()
.find(|short| !meta.extra.hidden_shorts.contains(short))
.map(|short| short_form(*short))
})
.or_else(|| meta.flag.negate.map(long_form))
.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() => long_form(negate),
Some(negate) if show.long.is_none() && show.short.is_none() => {
cat(&[&usage, " --", negate])
}
Some(negate) => cat(&[&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) = find_str(&rest, &long_form(long)) else {
return rest;
};
let (before, after) = rest.split_at(at);
let short = before.trimmed();
let bare_short = short.is_empty()
|| (short.starts_with('-') && !short.starts_with("--") && char_count(short) == 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.put_char('\n');
}
if let Some(header) = example.header {
put(out, &[" ", header, ":\n"]);
}
if let Some(help) = example.help.filter(|_| long) {
line_at(out, 4, help);
}
put(out, &[" $ ", example.code, "\n"]);
}
}
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()
}
#[inline(never)]
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) {
for (i, line) in lines_of(text).into_iter().enumerate() {
if i == 0 || !line.is_empty() {
line_at(out, indent, line);
} else {
out.put_char('\n');
}
}
if text.ends_with('\n') {
out.put_char('\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 = char_count(usage) > col;
let inline_start = if overflow {
2 + char_count(usage) + 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.is_blank()) else {
line_at(out, 2, painted);
return if block { block_indent } else { indent };
};
if block {
line_at(out, 2, painted);
write_wrapped_indented(out, help, width, block_indent);
return block_indent;
}
if overflow {
wrap_lead(
out,
&[" ", painted, " "],
help,
inline_room,
room,
indent,
true,
);
return indent;
}
out.put_str(" ");
out.put_str(painted);
for _ in 0..col.saturating_sub(char_count(usage)) {
out.put_char(' ');
}
out.put_str(" ");
if fits(help, room) {
out.put_str(help);
out.put_char('\n');
return indent;
}
wrap_lead(out, &[], help, room, room, indent, false);
indent
}
fn wrap_lead(
out: &mut String,
lead: &[&str],
text: &str,
first_width: usize,
width: usize,
indent: usize,
per_line: bool,
) {
let mut first = true;
let mut emit = |line: &str| {
if first {
first = false;
put(out, lead);
out.put_str(line);
out.put_char('\n');
} else if line.is_empty() {
out.put_char('\n');
} else {
line_at(out, indent, line);
}
};
if per_line {
let mut index = 0;
let mut rest = Some(text);
while let Some(remaining) = rest {
let (line, next) = split_line(remaining);
rest = next;
wrap_lines(
line,
if index == 0 { first_width } else { width },
&mut emit,
);
index += 1;
}
} else {
wrap_lines(text, width, &mut emit);
}
}
fn write_wrapped_block(out: &mut String, help: &str, width: usize) {
write_wrapped_indented(out, help, width, BLOCK_INDENT);
}
#[inline(never)]
fn write_wrapped_indented(out: &mut String, help: &str, width: usize, indent: usize) {
let room = width.saturating_sub(indent);
wrap_lines(help, room, &mut |line| {
if line.is_empty() {
out.put_char('\n');
} else {
line_at(out, indent, 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
}
#[inline(never)]
fn wrap_lines(text: &str, width: usize, emit: &mut dyn FnMut(&str)) {
let mut emitted = false;
let mut line = String::new();
let mut rest = Some(text);
while let Some(remaining) = rest {
let (paragraph, next) = split_line(remaining);
rest = next;
if paragraph.is_empty() {
emit("");
emitted = true;
continue;
}
if paragraph.starts_with(" ") || paragraph.starts_with('\t') {
emit(paragraph);
emitted = true;
continue;
}
let trimmed = paragraph.trimmed();
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 prefix_width = char_count(prefix);
let body_width = width.saturating_sub(prefix_width);
line.clear();
line.put_str(prefix);
let mut words = false;
let mut line_width = 0;
for word in body.split_whitespace() {
let word_width = char_count(word);
if words && line_width + 1 + word_width > body_width {
emit(&line);
emitted = true;
line.clear();
put_pad(&mut line, prefix_width);
words = false;
line_width = 0;
}
if words {
line.put_char(' ');
line_width += 1;
}
line.put_str(word);
words = true;
line_width += word_width;
}
if words {
emit(&line);
emitted = true;
}
}
if !emitted {
emit("");
}
}
#[cfg(test)]
fn wrap(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
wrap_lines(text, width, &mut |line| lines.push(line.to_string()));
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;
}
let mut note = |prefix: &str, body: &str, suffix: &str| {
write_wrapped_indented(out, &cat(&[prefix, body, suffix]), width, indent);
};
if !choices.is_empty() {
note("[possible values: ", &joined(choices, ", "), "]");
}
if let Some(env) = env {
note("[env: ", env, "]");
}
for env in env_fallback {
note("[env fallback: ", env, "]");
}
for env in deprecated_env {
note("[deprecated env: ", env, "]");
}
if !default.is_empty() {
note("(default: ", &joined(default, ", "), ")");
}
}
#[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 continuation = indent + char_count(label) + 2;
let mut lead = String::new();
put_pad(&mut lead, indent);
put(&mut lead, &[label, ": "]);
let room = width.saturating_sub(continuation);
wrap_lead(out, &[&lead], text, room, room, continuation, false);
}
fn summarize(text: Option<&str>) -> Option<&str> {
text.map(|text| text.trimmed_end())
.filter(|text| !text.is_empty())
}
#[inline(never)]
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 out = String::from("[deprecated: ");
let mut first = true;
for (lead, part) in [
("", message),
("warns at ", warn_at),
("removed at ", remove_at),
] {
if let Some(part) = part {
if !first {
out.put_str("; ");
}
first = false;
put(&mut out, &[lead, part]);
}
}
out.put_char(']');
Some(out)
}
#[inline(never)]
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 out = String::new();
for (label, names) in [
("[env fallback: ", fallbacks),
("[deprecated env: ", deprecated),
] {
for name in names {
if !out.is_empty() {
out.put_char(' ');
}
put(&mut out, &[label, name, "]"]);
}
}
(!out.is_empty()).then_some(out)
}
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: &[Form<'_>],
) -> Vec<&'static FlagMeta<'static>> {
let pick = |long: &str, short: u8, both, l, s| match (
taken.contains(&Form::Long(long)),
taken.contains(&Form::Short(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",
b'h',
&supplied::HELP_BOTH,
&supplied::HELP_LONG_ONLY,
&supplied::HELP_SHORT_ONLY,
));
}
if cmd.version && !cmd.disable_version_flag {
out.extend(pick(
"version",
b'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 negation<'f>(f: &'f FlagMeta<'f>) -> Option<Form<'f>> {
f.flag.negate.map(Form::Long)
}
let mut taken: Vec<Form<'_>> = Vec::new();
let mut taken_negations: Vec<Form<'_>> = Vec::new();
for f in here.flags {
push_forms(&mut taken, f);
if let Some(form) = negation(f) {
taken_negations.push(form);
}
}
let mut inherited = if ancestors.is_empty() {
Vec::new()
} else {
let visits = |f: &FlagMeta<'_>| {
f.flag.global || (inherit_version_actions && crate::is_version_flag(f.flag))
};
let mut every_form: Vec<Form<'_>> = taken.clone();
for meta in ancestors {
for f in meta.flags.iter().filter(|f| visits(f)) {
push_forms(&mut every_form, f);
}
}
let mut keep: Vec<(*const FlagMeta<'_>, Shown<'_>)> = Vec::new();
for meta in ancestors.iter().rev() {
for f in meta.flags.iter().filter(|f| visits(f)) {
let show = Shown::surviving(f, &taken, &taken_negations, &every_form);
push_forms(&mut taken, f);
if let Some(form) = negation(f) {
taken_negations.push(form);
}
if f.hide || show.nothing() {
continue;
}
keep.push((f as *const _, show));
}
}
let mut inherited: Vec<(&FlagMeta<'_>, String)> = Vec::new();
let mut inherited_positions: Vec<*const FlagMeta<'_>> = Vec::new();
for f in ancestors.iter().flat_map(|meta| meta.flags.iter()) {
if let Some((_, show)) = keep.iter().find(|(p, _)| core::ptr::eq(*p, f as *const _)) {
inherited.push((f, column_usage_masked(f, show)));
inherited_positions.push(f as *const _);
}
}
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<Form<'_>> = taken.iter().chain(&taken_negations).copied().collect();
if inherit_version_actions && 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.put_char('-');
}
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.put_str("\n\n");
}
existing.put_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.put_char('\n');
}
block.put_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.put_char('\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> {
let (path, chain) = match route_to(root, argv, cmd) {
Some(route) => route_context(spec, &route)?,
None => find(spec, cmd)?,
};
Some(match page {
Page::Short | Page::Long => assembled_help(
spec,
&path,
&chain,
matches!(page, Page::Long),
style,
false,
true,
),
Page::All => recursive_help(spec, path, chain, style, false),
})
}
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.put_char('\n');
}
out.put_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::{
AnnotationLayout, Palette, Row, Shown, Style, 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,
};
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]
#[cfg(feature = "help-advanced")]
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");
}