use core::fmt::Write as _;
use crate::spec::{ArgMeta, CommandMeta, Example, FlagMeta, Spec, ViewMeta};
use crate::Command;
use crate::DoubleDash;
const INLINE_LIMIT: usize = 2;
pub const SECTIONS: [&str; 6] = ["about", "usage", "commands", "args", "flags", "after_help"];
pub fn unsupported_section(template: &str) -> Result<Option<&str>, &'static str> {
let mut rest = template;
while let Some(at) = rest.find("{{") {
let after = &rest[at + 2..];
let Some(end) = after.find("}}") else {
return Err("a `{{` with no `}}` after it");
};
let name = after[..end].trim();
if !SECTIONS.contains(&name) {
return Ok(Some(name));
}
rest = &after[end + 2..];
}
Ok(None)
}
#[derive(Default)]
struct Sections {
about: String,
usage: String,
commands: String,
args: String,
flags: String,
flattened: String,
after_help: String,
}
impl Sections {
fn concatenated(&self) -> String {
let mut out = String::new();
for part in [
&self.about,
&self.usage,
&self.commands,
&self.args,
&self.flags,
&self.flattened,
&self.after_help,
] {
out.push_str(part);
}
out
}
fn named(&self, name: &str) -> Option<String> {
Some(match name {
"about" => self.about.trim().to_string(),
"usage" => self.usage.trim().to_string(),
"commands" => {
let mut out = self.commands.trim().to_string();
let flattened = self.flattened.trim();
if !flattened.is_empty() {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str(flattened);
}
out
}
"args" => self.args.trim().to_string(),
"flags" => self.flags.trim().to_string(),
"after_help" => self.after_help.trim().to_string(),
_ => return None,
})
}
fn substituted(&self, template: &str) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(at) = rest.find("{{") {
out.push_str(&rest[..at]);
let after = &rest[at + 2..];
let Some(end) = after.find("}}") else {
out.push_str(&rest[at..]);
return collapse_blank_runs(&out);
};
match self.named(after[..end].trim()) {
Some(text) => out.push_str(&text),
None => out.push_str(&rest[at..at + 2 + end + 2]),
}
rest = &after[end + 2..];
}
out.push_str(rest);
collapse_blank_runs(&out)
}
}
fn collapse_blank_runs(page: &str) -> String {
let mut out = String::with_capacity(page.len());
let mut blank = false;
for line in page.split('\n') {
if line.trim().is_empty() {
blank = !out.is_empty();
continue;
}
if !out.is_empty() {
out.push('\n');
if blank {
out.push('\n');
}
}
blank = false;
out.push_str(line);
}
out
}
fn assemble(spec: &Spec<'_>, sections: &Sections) -> String {
let page = match spec
.help_template
.filter(|template| !template.trim().is_empty())
{
Some(template) => sections.substituted(template),
None => sections.concatenated(),
};
let trimmed = page.trim();
let mut done = String::with_capacity(trimmed.len() + 1);
done.push_str(trimmed);
done.push('\n');
done
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Style {
coloured: bool,
}
impl Style {
pub const PLAIN: Style = Style { coloured: false };
pub const COLOURED: Style = Style { coloured: true };
pub fn auto() -> Style {
use std::io::IsTerminal as _;
Self::auto_for(std::io::stdout().is_terminal())
}
pub fn auto_stderr() -> Style {
use std::io::IsTerminal as _;
Self::auto_for(std::io::stderr().is_terminal())
}
fn auto_for(is_terminal: bool) -> Style {
let forced = std::env::var_os("CLICOLOR_FORCE").is_some_and(|v| v != "0");
let refused = std::env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty());
if refused {
Style::PLAIN
} else if forced || is_terminal {
Style::COLOURED
} else {
Style::PLAIN
}
}
fn wrap(self, code: &str, text: &str) -> String {
if self.coloured {
format!("\u{1b}[{code}m{text}\u{1b}[0m")
} else {
text.to_string()
}
}
fn heading(self, text: &str) -> String {
self.wrap("1;4;32", text)
}
fn literal(self, text: &str) -> String {
self.wrap("36", text)
}
}
fn styled_flag_usage(usage: &str, style: Style) -> String {
let mut out = String::with_capacity(usage.len());
let mut rest = usage;
while let Some(start) = rest.find('-') {
let previous_allows = start == 0
|| rest[..start]
.chars()
.next_back()
.is_some_and(|c| c.is_whitespace() || matches!(c, ',' | ':' | '[' | '<'));
if !previous_allows {
out.push_str(&rest[..=start]);
rest = &rest[start + 1..];
continue;
}
let end = rest[start..]
.char_indices()
.skip(1)
.find_map(|(i, c)| {
(c.is_whitespace() || matches!(c, ',' | '=' | '[' | ']' | '>')).then_some(i)
})
.unwrap_or(rest.len() - start)
+ start;
out.push_str(&rest[..start]);
out.push_str(&style.literal(&rest[start..end]));
rest = &rest[end..];
}
out.push_str(rest);
out
}
fn help_structure(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
long: bool,
inherit_version_actions: bool,
) -> (Vec<String>, Vec<String>, Vec<String>) {
let meta = *chain.last().expect("a page is always about some command");
let mut headings = Vec::new();
if !page_examples(spec, meta).is_empty() {
headings.push("Examples".to_string());
}
if meta.flatten_help {
flat_help_headings(&path[1.min(path.len())..], meta, &mut headings);
} else if meta.subcommands.iter().any(|sub| !sub.hide) {
headings.push(
meta.subcommand_help_heading
.unwrap_or("Commands")
.to_string(),
);
headings.extend(
meta.subcommands
.iter()
.filter(|sub| !sub.hide)
.filter_map(|sub| sub.help_heading)
.map(str::to_string),
);
}
let (own, inherited) = own_and_global(chain, inherit_version_actions);
let visible_arg = |arg: &&ArgMeta<'_>| {
!arg.hide
&& if long {
!arg.hide_long_help
} else {
!arg.hide_short_help
}
};
let mut args: Vec<_> = meta.args.iter().filter(visible_arg).collect();
order_args(&mut args, meta.args);
if args.iter().any(|arg| arg.help_heading.is_none()) {
headings.push("Arguments".to_string());
}
headings.extend(
args.iter()
.filter_map(|arg| arg.help_heading)
.map(str::to_string),
);
let visible_flag = |flag: &&FlagMeta<'_>| {
!flag.hide
&& if long {
!flag.hide_long_help
} else {
!flag.hide_short_help
}
};
let mut own: Vec<_> = own.into_iter().filter(visible_flag).collect();
order_flags(&mut own, meta.flags);
let inherited: Vec<_> = inherited
.into_iter()
.filter(|(flag, _)| {
if long {
!flag.hide_long_help
} else {
!flag.hide_short_help
}
})
.collect();
if own.iter().any(|flag| flag.help_heading.is_none()) {
headings.push("Flags".to_string());
}
headings.extend(
own.iter()
.filter_map(|flag| flag.help_heading)
.map(str::to_string),
);
if !inherited.is_empty() {
headings.push("Global flags".to_string());
}
let mut flag_usages: Vec<String> = own.iter().map(|flag| column_usage(flag)).collect();
flag_usages.extend(inherited.into_iter().map(|(_, usage)| usage));
flag_usages.sort_by_key(|usage| core::cmp::Reverse(usage.len()));
let mut synopsis = String::new();
usage_section(&mut synopsis, spec, path, meta);
let synopsis = synopsis.lines().map(str::to_string).collect();
(headings, flag_usages, synopsis)
}
fn flat_help_headings(path: &[&str], meta: &CommandMeta<'_>, headings: &mut Vec<String>) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
headings.push(sub_path.join(" "));
if sub.flatten_help {
flat_help_headings(&sub_path, sub, headings);
}
}
}
fn styled_help(
page: &str,
style: Style,
headings: &[String],
flag_usages: &[String],
synopsis: &[String],
) -> String {
if !style.coloured {
return page.to_string();
}
let mut out = String::with_capacity(page.len());
for line in page.split_inclusive('\n') {
let (body, newline) = line
.strip_suffix('\n')
.map_or((line, ""), |body| (body, "\n"));
if synopsis.iter().any(|known| known == body) && body.starts_with("Usage:") {
let usage = body.strip_prefix("Usage:").unwrap_or_default();
out.push_str(&style.heading("Usage:"));
out.push_str(&style.literal(usage));
} else if synopsis.iter().any(|known| known == body) {
out.push_str(&style.literal(body));
} else if body
.strip_suffix(':')
.is_some_and(|heading| headings.iter().any(|known| known == heading))
{
out.push_str(&style.heading(body));
} else {
let styled = body.strip_prefix(" ").and_then(|entry| {
flag_usages.iter().find_map(|usage| {
entry
.strip_prefix(usage)
.filter(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
.map(|rest| format!(" {}{rest}", styled_flag_usage(usage, style)))
})
});
out.push_str(styled.as_deref().unwrap_or(body));
}
out.push_str(newline);
}
out
}
pub fn usage_line(path: &[&str], meta: &CommandMeta<'_>) -> String {
usage_line_with_subcommands(path, meta, true)
}
fn usage_line_with_subcommands(
path: &[&str],
meta: &CommandMeta<'_>,
include_subcommands: bool,
) -> String {
let mut out = String::new();
for (i, part) in path.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(part);
}
let flags: usize = meta.flags.iter().filter(|f| !f.hide).count();
if flags > 0 {
let required = meta.flags.iter().any(|f| !f.hide && flag_demanded(f));
if flags <= INLINE_LIMIT {
for flag in meta.flags.iter().filter(|f| !f.hide) {
let (open, close) = if flag_demanded(flag) {
('<', '>')
} else {
('[', ']')
};
let _ = write!(out, " {open}{}{close}", flag_usage(flag));
}
} else if required {
out.push_str(" <FLAGS>");
} else {
out.push_str(" [FLAGS]");
}
}
let args: usize = meta.args.iter().filter(|a| !a.hide).count();
if args > 0 {
let required = meta.args.iter().any(|a| !a.hide && demanded(a));
if args <= INLINE_LIMIT {
for arg in meta.args.iter().filter(|a| !a.hide) {
let _ = write!(out, " {}", arg_usage(arg));
}
} else if required {
out.push_str(" <ARGS>…");
} else {
out.push_str(" [ARGS]…");
}
}
if include_subcommands && !meta.cmd.subcommands.is_empty() {
let name = meta.subcommand_value_name.unwrap_or("SUBCOMMAND");
let _ = write!(out, " <{name}>");
}
out
}
fn usage_section(out: &mut String, spec: &Spec<'_>, path: &[&str], meta: &CommandMeta<'_>) {
if path.len() <= 1 {
if let Some(usage) = spec.usage.filter(|usage| !usage.trim().is_empty()) {
let _ = writeln!(out, "{}", usage.trim());
return;
}
}
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
visible.sort_by_key(|sub| sub.cmd.name);
if meta.flatten_help && !visible.is_empty() {
let mut lines = Vec::new();
if !meta.subcommand_required || meta.cmd.args_conflicts_with_subcommands {
lines.push(usage_line_with_subcommands(path, meta, false));
}
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
lines.push(usage_line(&sub_path, sub));
}
if let Some((first, rest)) = lines.split_first() {
let _ = writeln!(out, "Usage: {first}");
for line in rest {
let _ = writeln!(out, " {line}");
}
}
} else {
let _ = writeln!(out, "Usage: {}", usage_line(path, meta));
}
}
fn flag_usage(meta: &FlagMeta<'_>) -> String {
flag_usage_masked(meta, &Shown::all(meta))
}
struct Shown<'a> {
long: Option<&'a str>,
short: Option<u8>,
negate: bool,
}
impl<'a> Shown<'a> {
fn all(meta: &'a FlagMeta<'a>) -> Self {
Shown {
long: meta
.flag
.longs
.iter()
.copied()
.find(|long| !meta.hidden_longs.contains(long)),
short: meta
.flag
.shorts
.iter()
.copied()
.find(|short| !meta.hidden_shorts.contains(short)),
negate: meta.flag.negate.is_some(),
}
}
fn surviving(
meta: &'a FlagMeta<'a>,
taken: &[String],
taken_negations: &[String],
every_form: &[String],
) -> Self {
let mine: Vec<String> = meta
.flag
.longs
.iter()
.map(|l| format!("--{l}"))
.chain(meta.flag.shorts.iter().map(|s| format!("-{}", *s as char)))
.collect();
Shown {
long: meta
.flag
.longs
.iter()
.copied()
.find(|l| !meta.hidden_longs.contains(l) && !taken.contains(&format!("--{l}"))),
short: meta.flag.shorts.iter().copied().find(|s| {
!meta.hidden_shorts.contains(s) && !taken.contains(&format!("-{}", *s as char))
}),
negate: meta.flag.negate.is_some_and(|n| {
let spelling = format!("--{n}");
!taken_negations.contains(&spelling)
&& (!every_form.contains(&spelling) || mine.contains(&spelling))
}),
}
}
fn nothing(&self) -> bool {
self.long.is_none() && self.short.is_none() && !self.negate
}
}
fn flag_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
let flag = meta.flag;
let mut out = String::new();
let long = show.long;
let short = show.short.as_ref();
let implied = long.or_else(|| short.map(|_| ""));
let implied_matches = match (implied, short) {
(Some(long), _) if !long.is_empty() => long == flag.name,
(Some(_), Some(short)) => {
let mut buf = [0u8; 4];
(*short as char).encode_utf8(&mut buf) == flag.name
}
_ => show.negate && flag.negate == Some(flag.name),
};
if !implied_matches {
let _ = write!(out, "{}:", flag.name);
}
if let Some(short) = short {
if !out.is_empty() {
out.push(' ');
}
let _ = write!(out, "-{}", *short as char);
}
if let Some(long) = long {
if !out.is_empty() {
out.push(' ');
}
let _ = write!(out, "--{long}");
}
if meta.repeatable {
out.push('…');
}
if flag.takes_value {
let exact = exact_arity(meta.value_var_min, meta.value_var_max);
if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
for index in 0..exact.unwrap() {
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
index == 0,
);
}
} else if meta.value_names.len() <= 1 {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
true,
);
} else {
for (index, name) in meta.value_names.iter().enumerate() {
append_flag_value(
&mut out,
name,
meta.value_optional,
flag.require_equals,
index == 0,
);
}
}
if flag.variadic && meta.value_names.len() <= 1 && exact.is_none() {
out.push('…');
}
}
out
}
fn append_flag_value(
out: &mut String,
name: &str,
optional: bool,
require_equals: bool,
first: bool,
) {
if first && optional && require_equals {
let _ = write!(out, "[={name}]");
} else {
let separator = if first && require_equals { "=" } else { " " };
let (open, close) = if optional { ('[', ']') } else { ('<', '>') };
let _ = write!(out, "{separator}{open}{name}{close}");
}
}
fn flag_demanded(meta: &FlagMeta<'_>) -> bool {
meta.required && meta.default.is_empty()
}
fn demanded(meta: &ArgMeta<'_>) -> bool {
meta.required && meta.default.is_empty()
}
pub(crate) fn arg_usage(meta: &ArgMeta<'_>) -> String {
let arg = meta.arg;
let mut out = String::new();
let (open, close) = if demanded(meta) {
('<', '>')
} else {
('[', ']')
};
let exact = exact_arity(meta.var_min, meta.var_max);
if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
for index in 0..exact.unwrap() {
if index > 0 {
out.push(' ');
}
let _ = write!(out, "{open}{}{close}", arg.name);
}
} else if meta.value_names.len() <= 1 {
if arg.double_dash == DoubleDash::Required {
let _ = write!(out, "{open}-- {}{close}", arg.name);
} else {
let _ = write!(out, "{open}{}{close}", arg.name);
}
} else {
if arg.double_dash == DoubleDash::Required {
out.push_str("-- ");
}
for (index, name) in meta.value_names.iter().enumerate() {
if index > 0 {
out.push(' ');
}
let _ = write!(out, "{open}{name}{close}");
}
}
if arg.var && meta.value_names.len() <= 1 && exact.is_none() {
out.push('…');
}
out
}
fn exact_arity(min: Option<usize>, max: Option<usize>) -> Option<usize> {
match (min, max) {
(Some(min), Some(max)) if min == max => Some(min),
_ => None,
}
}
pub fn short_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) -> String {
short_help_with(spec, path, chain, false)
}
fn short_help_with(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> String {
let meta = *chain.last().expect("a page is always about some command");
let (own, inherited) = own_and_global(chain, inherit_version_actions);
let own: Vec<_> = own
.into_iter()
.filter(|flag| !flag.hide_short_help)
.collect();
let inherited: Vec<_> = inherited
.into_iter()
.filter(|(flag, _)| !flag.hide_short_help)
.collect();
let mut sections = Sections::default();
let out = &mut sections.about;
if let Some(before) = meta.before_help.or(spec.root.before_help) {
let _ = writeln!(out, "{before}\n");
}
let root = path.len() <= 1;
if root {
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
}
}
let about = if root { spec.about } else { meta.about };
if let Some(about) = about {
let _ = writeln!(out, "{}\n", about.trim_end());
}
command_deprecation(out, meta, 0);
usage_section(&mut sections.usage, spec, path, meta);
if !meta.flatten_help {
commands_section(&mut sections.commands, &path[1.min(path.len())..], meta);
}
let mut args: Vec<&ArgMeta<'_>> = meta
.args
.iter()
.filter(|a| !a.hide && !a.hide_short_help)
.collect();
order_args(&mut args, meta.args);
let arg_col = args
.iter()
.map(|a| arg_usage(a).chars().count())
.max()
.unwrap_or(0);
groups_section(
&mut sections.args,
"Arguments",
args.iter().copied(),
|a| a.help_heading,
|out, a| {
let usage = arg_usage(a);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = a.help.filter(|h| !h.trim().is_empty()) {
write_indented(out, help, 4);
}
long_annotations(
out,
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
if a.hide_env { &[] } else { a.env_fallback },
if a.hide_env { &[] } else { a.deprecated_env },
if a.hide_default_value { &[] } else { a.default },
);
return;
}
match a.help.filter(|h| !h.trim().is_empty()) {
Some(help) => {
let _ = write!(out, " {usage:<arg_col$} {help}");
}
None => {
let _ = write!(out, " {usage}");
}
}
let environment =
inline_environment_notes(a.hide_env, a.env_fallback, a.deprecated_env);
annotations(
out,
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
environment.as_deref(),
if a.hide_default_value { &[] } else { a.default },
None,
);
},
);
let flag_col = own
.iter()
.map(|f| column_usage(f).chars().count())
.chain(inherited.iter().map(|(_, u)| u.chars().count()))
.max()
.unwrap_or(0);
let short_entry = |out: &mut String, f: &FlagMeta<'_>, usage: String| {
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = f.help.filter(|h| !h.trim().is_empty()) {
write_indented(out, help, 4);
}
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_env { &[] } else { f.env_fallback },
if f.hide_env { &[] } else { f.deprecated_env },
if f.hide_default_value { &[] } else { f.default },
);
flag_notes(out, f, 4);
return;
}
match f.help.filter(|h| !h.trim().is_empty()) {
Some(help) => {
let _ = write!(out, " {usage:<flag_col$} {help}");
}
None => {
let _ = write!(out, " {usage}");
}
}
let deprecation =
deprecation_label(f.deprecated, f.deprecated_warn_at, f.deprecated_remove_at);
let environment = inline_environment_notes(f.hide_env, f.env_fallback, f.deprecated_env);
annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
environment.as_deref(),
if f.hide_default_value { &[] } else { f.default },
deprecation.as_deref(),
);
};
groups_section(
&mut sections.flags,
"Flags",
own.iter().copied(),
|f| f.help_heading,
|out, f| short_entry(out, f, column_usage(f)),
);
groups_section(
&mut sections.flags,
"Global flags",
inherited.iter(),
|_| None,
|out, (f, usage)| short_entry(out, f, usage.clone()),
);
if meta.flatten_help {
flat_commands_short(&mut sections.flattened, &path[1.min(path.len())..], meta);
}
examples_section(&mut sections.after_help, spec, meta);
if let Some(after) = meta.after_help.or(spec.root.after_help) {
let _ = writeln!(sections.after_help, "\n{after}");
}
assemble(spec, §ions)
}
fn commands_section(out: &mut String, path: &[&str], meta: &CommandMeta<'_>) {
let mut visible: Vec<&&CommandMeta<'_>> = meta.subcommands.iter().filter(|c| !c.hide).collect();
order_commands(&mut visible);
if visible.is_empty() {
return;
}
let mut lines: Vec<(String, &&CommandMeta<'_>)> = visible
.iter()
.map(|sub| {
let mut sub_path: Vec<&str> = path.to_vec();
sub_path.push(sub.cmd.name);
(usage_line(&sub_path, sub), *sub)
})
.collect();
lines.sort_by(|a, b| {
a.1.display_order
.unwrap_or(999)
.cmp(&b.1.display_order.unwrap_or(999))
.then_with(|| a.0.cmp(&b.0))
});
let default_title = meta.subcommand_help_heading.unwrap_or("Commands");
let mut headings = vec![None];
for (_, sub) in &lines {
let heading = command_help_section(sub, default_title);
if !headings.contains(&heading) {
headings.push(heading);
}
}
for heading in headings {
let title = heading.unwrap_or(default_title);
let _ = writeln!(out, "\n{title}:");
for (usage, sub) in lines
.iter()
.filter(|(_, sub)| command_help_section(sub, default_title) == heading)
{
let _ = write!(out, " {usage}");
let visible_aliases: Vec<&str> = sub
.cmd
.aliases
.iter()
.copied()
.filter(|a| !sub.hidden_aliases.contains(a))
.collect();
if !visible_aliases.is_empty() {
let _ = write!(out, " [aliases: {}]", visible_aliases.join(", "));
}
if let Some(about) = sub.about {
if meta.next_line_help {
out.push('\n');
write_indented(out, about.trim_end(), 4);
continue;
}
let _ = write!(out, " {}", about.trim_end());
}
if let Some(label) = deprecation_label(
sub.deprecated,
sub.deprecated_warn_at,
sub.deprecated_remove_at,
) {
let _ = write!(out, " {label}");
}
out.push('\n');
}
if heading.is_none() && !meta.cmd.disable_help_subcommand {
if meta.next_line_help {
let _ = writeln!(
out,
" help\n Print this message or the help of the given subcommand(s)"
);
} else {
let _ = writeln!(
out,
" help Print this message or the help of the given subcommand(s)"
);
}
}
}
}
fn flat_commands_short(out: &mut String, path: &[&str], meta: &CommandMeta<'_>) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
let _ = writeln!(out, "\n{}:", sub_path.join(" "));
if let Some(about) = sub.about.filter(|about| !about.trim().is_empty()) {
let _ = writeln!(out, "{}", about.trim_end());
}
command_deprecation(out, sub, 0);
let mut args: Vec<_> = sub
.args
.iter()
.filter(|arg| !arg.hide && !arg.hide_short_help)
.collect();
order_args(&mut args, sub.args);
let mut flags: Vec<&FlagMeta<'_>> = sub
.flags
.iter()
.filter(|flag| !flag.flag.global && !flag.hide && !flag.hide_short_help)
.collect();
order_flags(&mut flags, sub.flags);
let arg_col = args
.iter()
.map(|arg| arg_usage(arg).chars().count())
.max()
.unwrap_or(0);
let flag_col = flags
.iter()
.map(|flag| column_usage(flag).chars().count())
.max()
.unwrap_or(0);
for arg in args {
let usage = arg_usage(arg);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = arg.help.filter(|help| !help.trim().is_empty()) {
write_indented(out, help, 4);
}
long_annotations(
out,
if arg.hide_possible_values {
&[]
} else {
arg.choices
},
if arg.hide_env { None } else { arg.env },
if arg.hide_env { &[] } else { arg.env_fallback },
if arg.hide_env {
&[]
} else {
arg.deprecated_env
},
if arg.hide_default_value {
&[]
} else {
arg.default
},
);
continue;
}
if let Some(help) = arg.help.filter(|help| !help.trim().is_empty()) {
let _ = write!(out, " {usage:<arg_col$} {help}");
} else {
let _ = write!(out, " {usage}");
}
let environment =
inline_environment_notes(arg.hide_env, arg.env_fallback, arg.deprecated_env);
annotations(
out,
if arg.hide_possible_values {
&[]
} else {
arg.choices
},
if arg.hide_env { None } else { arg.env },
environment.as_deref(),
if arg.hide_default_value {
&[]
} else {
arg.default
},
None,
);
}
for flag in flags {
let usage = column_usage(flag);
if meta.next_line_help {
let _ = writeln!(out, " {usage}");
if let Some(help) = flag.help.filter(|help| !help.trim().is_empty()) {
write_indented(out, help, 4);
}
long_annotations(
out,
if flag.hide_possible_values {
&[]
} else {
flag.choices
},
if flag.hide_env { None } else { flag.env },
if flag.hide_env {
&[]
} else {
flag.env_fallback
},
if flag.hide_env {
&[]
} else {
flag.deprecated_env
},
if flag.hide_default_value {
&[]
} else {
flag.default
},
);
flag_notes(out, flag, 4);
continue;
}
if let Some(help) = flag.help.filter(|help| !help.trim().is_empty()) {
let _ = write!(out, " {usage:<flag_col$} {help}");
} else {
let _ = write!(out, " {usage}");
}
let deprecation = deprecation_label(
flag.deprecated,
flag.deprecated_warn_at,
flag.deprecated_remove_at,
);
let environment =
inline_environment_notes(flag.hide_env, flag.env_fallback, flag.deprecated_env);
annotations(
out,
if flag.hide_possible_values {
&[]
} else {
flag.choices
},
if flag.hide_env { None } else { flag.env },
environment.as_deref(),
if flag.hide_default_value {
&[]
} else {
flag.default
},
deprecation.as_deref(),
);
}
if sub.flatten_help {
flat_commands_short(out, &sub_path, sub);
}
out.push('\n');
}
}
fn groups_section<'m, T: 'm>(
out: &mut String,
default_title: &str,
items: impl Iterator<Item = &'m T> + Clone,
heading_of: impl Fn(&T) -> Option<&'m str>,
mut write_item: impl FnMut(&mut String, &T),
) {
let mut headings: Vec<Option<&str>> = Vec::new();
for item in items.clone() {
let heading = heading_of(item);
if !headings.contains(&heading) {
headings.push(heading);
}
}
headings.sort_by_key(|h| h.is_some());
for heading in headings {
let _ = writeln!(out, "\n{}:", heading.unwrap_or(default_title));
for item in items.clone().filter(|i| heading_of(i) == heading) {
write_item(out, item);
}
}
}
fn order_args<'a>(items: &mut Vec<&'a ArgMeta<'a>>, declared: &'a [ArgMeta<'a>]) {
items.sort_by_key(|item| {
item.display_order.unwrap_or_else(|| {
declared
.iter()
.position(|candidate| core::ptr::eq(candidate, *item))
.unwrap_or(usize::MAX)
})
});
}
fn order_flags<'a>(items: &mut Vec<&'a FlagMeta<'a>>, declared: &'a [FlagMeta<'a>]) {
items.sort_by_key(|item| {
item.display_order.unwrap_or_else(|| {
declared
.iter()
.position(|candidate| core::ptr::eq(candidate, *item))
.unwrap_or(usize::MAX)
})
});
}
fn order_commands(items: &mut Vec<&&CommandMeta<'_>>) {
items.sort_by(|a, b| {
a.display_order
.unwrap_or(999)
.cmp(&b.display_order.unwrap_or(999))
.then_with(|| a.cmd.name.cmp(b.cmd.name))
});
}
fn command_help_section<'a>(sub: &'a CommandMeta<'a>, default_title: &str) -> Option<&'a str> {
sub.help_heading.filter(|heading| *heading != default_title)
}
fn annotations(
out: &mut String,
choices: &[&str],
env: Option<&str>,
environment: Option<&str>,
default: &[&str],
suffix: Option<&str>,
) {
if !choices.is_empty() {
let _ = write!(out, " [{}]", choices.join(", "));
}
if let Some(env) = env {
let _ = write!(out, " [env: {env}]");
}
if let Some(environment) = environment {
let _ = write!(out, " {environment}");
}
if !default.is_empty() {
let _ = write!(out, " (default: {})", default.join(", "));
}
if let Some(suffix) = suffix {
let _ = write!(out, " {suffix}");
}
out.push('\n');
}
#[cfg(feature = "diagnostics")]
pub(crate) fn flag_spelling(meta: &FlagMeta<'_>) -> String {
meta.flag
.longs
.iter()
.find(|long| !meta.hidden_longs.contains(long))
.map(|long| format!("--{long}"))
.or_else(|| {
meta.flag
.shorts
.iter()
.find(|short| !meta.hidden_shorts.contains(short))
.map(|short| format!("-{}", *short as char))
})
.or_else(|| meta.flag.negate.map(|negate| format!("--{negate}")))
.unwrap_or_else(|| meta.flag.name.to_string())
}
fn display_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
let usage = flag_usage_masked(meta, show);
match meta.flag.negate.filter(|_| show.negate) {
Some(negate) if usage.is_empty() => format!("--{negate}"),
Some(negate) if show.long.is_none() && show.short.is_none() => {
format!("{usage} --{negate}")
}
Some(negate) => format!("{usage} / --{negate}"),
None => usage,
}
}
const SHORT_COL: usize = 4;
fn column_usage(meta: &FlagMeta<'_>) -> String {
column_usage_masked(meta, &Shown::all(meta))
}
fn column_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
let rest = display_usage_masked(meta, show);
let Some(long) = show.long else {
return rest;
};
let Some(at) = rest.find(&format!("--{long}")) else {
return rest;
};
let (before, after) = rest.split_at(at);
let short = before.trim();
let bare_short = short.is_empty()
|| (short.starts_with('-') && !short.starts_with("--") && short.chars().count() == 2);
if !bare_short {
return rest;
}
let short = match short {
"" => String::new(),
s => format!("{s},"),
};
format!("{short:<SHORT_COL$}{after}")
}
fn examples_section(out: &mut String, spec: &Spec<'_>, meta: &CommandMeta<'_>) {
let examples = page_examples(spec, meta);
if examples.is_empty() {
return;
}
let _ = writeln!(out, "\nExamples:");
for example in examples {
if let Some(header) = example.header {
let _ = writeln!(out, " {header}:");
}
let _ = writeln!(out, " $ {}", example.code);
}
}
fn page_examples<'a>(spec: &Spec<'a>, meta: &CommandMeta<'a>) -> &'a [Example<'a>] {
if meta.examples.is_empty() {
spec.root.examples
} else {
meta.examples
}
}
fn terminal_width(meta: &CommandMeta<'_>) -> usize {
if let Some(width) = meta.term_width {
return if width == 0 { usize::MAX } else { width };
}
let detected = std::env::var("COLUMNS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(80);
match meta.max_term_width {
Some(0) | None => detected,
Some(max) => detected.min(max),
}
}
pub fn long_help(spec: &Spec<'_>, path: &[&str], chain: &[&CommandMeta<'_>]) -> String {
long_help_with(spec, path, chain, false)
}
fn long_help_with(
spec: &Spec<'_>,
path: &[&str],
chain: &[&CommandMeta<'_>],
inherit_version_actions: bool,
) -> String {
let meta = *chain.last().expect("a page is always about some command");
let (own, inherited) = own_and_global(chain, inherit_version_actions);
let own: Vec<_> = own
.into_iter()
.filter(|flag| !flag.hide_long_help)
.collect();
let inherited: Vec<_> = inherited
.into_iter()
.filter(|(flag, _)| !flag.hide_long_help)
.collect();
let width = terminal_width(meta);
let mut sections = Sections::default();
let out = &mut sections.about;
if let Some(before) = meta
.before_long_help
.or(meta.before_help)
.or(spec.root.before_long_help)
.or(spec.root.before_help)
{
let _ = writeln!(out, "{before}\n");
}
let root = path.len() <= 1;
if root {
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
}
}
let about = if root {
spec.long_about.or(spec.about)
} else {
meta.long_about.or(meta.about)
};
if let Some(about) = about {
let _ = writeln!(out, "{}\n", about.trim_end());
}
command_deprecation(out, meta, 0);
usage_section(&mut sections.usage, spec, path, meta);
if !meta.flatten_help {
long_commands_section(&mut sections.commands, &path[1.min(path.len())..], meta);
}
let mut args: Vec<&ArgMeta<'_>> = meta
.args
.iter()
.filter(|a| !a.hide && !a.hide_long_help)
.collect();
order_args(&mut args, meta.args);
let arg_col = args
.iter()
.map(|a| arg_usage(a).chars().count())
.max()
.unwrap_or(0);
groups_section(
&mut sections.args,
"Arguments",
args.iter().copied(),
|a| a.help_heading,
|out, a| {
let text = a.long_help.or(a.help);
entry(
out,
&arg_usage(a),
text,
arg_col,
width,
meta.next_line_help,
);
long_annotations(
out,
if a.hide_possible_values {
&[]
} else {
a.choices
},
if a.hide_env { None } else { a.env },
if a.hide_env { &[] } else { a.env_fallback },
if a.hide_env { &[] } else { a.deprecated_env },
if a.hide_default_value { &[] } else { a.default },
);
},
);
let flag_col = own
.iter()
.map(|f| column_usage(f).chars().count())
.chain(inherited.iter().map(|(_, u)| u.chars().count()))
.max()
.unwrap_or(0);
groups_section(
&mut sections.flags,
"Flags",
own.iter().copied(),
|f| f.help_heading,
|out, f| {
let text = f.long_help.or(f.help);
entry(
out,
&column_usage(f),
text,
flag_col,
width,
meta.next_line_help,
);
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_env { &[] } else { f.env_fallback },
if f.hide_env { &[] } else { f.deprecated_env },
if f.hide_default_value { &[] } else { f.default },
);
flag_notes(out, f, 4);
},
);
groups_section(
&mut sections.flags,
"Global flags",
inherited.iter(),
|_| None,
|out, (f, usage)| {
let text = f.long_help.or(f.help);
entry(out, usage, text, flag_col, width, meta.next_line_help);
long_annotations(
out,
if f.hide_possible_values {
&[]
} else {
f.choices
},
if f.hide_env { None } else { f.env },
if f.hide_env { &[] } else { f.env_fallback },
if f.hide_env { &[] } else { f.deprecated_env },
if f.hide_default_value { &[] } else { f.default },
);
flag_notes(out, f, 4);
},
);
if meta.flatten_help {
flat_commands_long(
&mut sections.flattened,
&path[1.min(path.len())..],
meta,
width,
);
}
let out = &mut sections.after_help;
let examples = page_examples(spec, meta);
if !examples.is_empty() {
let _ = writeln!(out, "\nExamples:");
for example in examples {
if let Some(header) = example.header {
let _ = writeln!(out, " {header}:");
}
if let Some(help) = example.help {
let _ = writeln!(out, " {help}");
}
let _ = writeln!(out, " $ {}", example.code);
}
}
let after = meta
.after_long_help
.or(meta.after_help)
.or(spec.root.after_long_help)
.or(spec.root.after_help);
if let Some(after) = after {
let _ = writeln!(out, "\n{after}");
}
if spec.author.is_some() || spec.license.is_some() {
out.push('\n');
if let Some(author) = spec.author {
let _ = writeln!(out, "Author: {author}");
}
if let Some(license) = spec.license {
let _ = writeln!(out, "License: {license}");
}
}
assemble(spec, §ions)
}
fn write_indented(out: &mut String, text: &str, indent: usize) {
let pad = " ".repeat(indent);
for (i, line) in text.lines().enumerate() {
if i == 0 || !line.is_empty() {
let _ = writeln!(out, "{pad}{line}");
} else {
out.push('\n');
}
}
if text.ends_with('\n') {
out.push('\n');
}
}
fn entry(
out: &mut String,
usage: &str,
help: Option<&str>,
col: usize,
width: usize,
next_line: bool,
) {
let Some(help) = help.filter(|h| !h.trim().is_empty()) else {
let _ = writeln!(out, " {usage}");
return;
};
let indent = 2 + col + 2;
let room = width.saturating_sub(indent);
if next_line || help.contains('\n') || room < 10 {
let _ = writeln!(out, " {usage}");
write_indented(out, help, 4);
return;
}
let lines = wrap(help, room);
let _ = writeln!(out, " {usage:<col$} {}", lines[0]);
for line in &lines[1..] {
let _ = writeln!(out, "{}{line}", " ".repeat(indent));
}
}
fn wrap(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
for paragraph in text.split('\n') {
if paragraph.is_empty() {
lines.push(String::new());
continue;
}
let mut line = String::new();
for word in paragraph.split_whitespace() {
let word_width = word.chars().count();
if !line.is_empty() && line.chars().count() + 1 + word_width > width {
lines.push(std::mem::take(&mut line));
}
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
}
if !line.is_empty() {
lines.push(line);
}
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
fn long_annotations(
out: &mut String,
choices: &[&str],
env: Option<&str>,
env_fallback: &[&str],
deprecated_env: &[&str],
default: &[&str],
) {
if !choices.is_empty() {
let _ = writeln!(out, " [possible values: {}]", choices.join(", "));
}
if let Some(env) = env {
let _ = writeln!(out, " [env: {env}]");
}
environment_notes(out, env_fallback, deprecated_env, 4);
if !default.is_empty() {
let _ = writeln!(out, " (default: {})", default.join(", "));
}
}
fn deprecation_label(
message: Option<&str>,
warn_at: Option<&str>,
remove_at: Option<&str>,
) -> Option<String> {
if message.is_none() && warn_at.is_none() && remove_at.is_none() {
return None;
}
let mut parts = Vec::new();
if let Some(message) = message {
parts.push(message.to_string());
}
if let Some(at) = warn_at {
parts.push(format!("warns at {at}"));
}
if let Some(at) = remove_at {
parts.push(format!("removed at {at}"));
}
Some(format!("[deprecated: {}]", parts.join("; ")))
}
fn command_deprecation(out: &mut String, meta: &CommandMeta<'_>, indent: usize) {
if let Some(label) = deprecation_label(
meta.deprecated,
meta.deprecated_warn_at,
meta.deprecated_remove_at,
) {
let _ = writeln!(out, "{}{label}", " ".repeat(indent));
}
}
fn inline_environment_notes(hide: bool, fallbacks: &[&str], deprecated: &[&str]) -> Option<String> {
let mut notes = Vec::new();
if !hide {
notes.extend(fallbacks.iter().map(|env| format!("[env fallback: {env}]")));
notes.extend(
deprecated
.iter()
.map(|env| format!("[deprecated env: {env}]")),
);
}
(!notes.is_empty()).then(|| notes.join(" "))
}
fn environment_notes(out: &mut String, fallbacks: &[&str], deprecated: &[&str], indent: usize) {
for env in fallbacks {
let _ = writeln!(out, "{}[env fallback: {env}]", " ".repeat(indent));
}
for env in deprecated {
let _ = writeln!(out, "{}[deprecated env: {env}]", " ".repeat(indent));
}
}
fn flag_notes(out: &mut String, meta: &FlagMeta<'_>, indent: usize) {
if let Some(label) = deprecation_label(
meta.deprecated,
meta.deprecated_warn_at,
meta.deprecated_remove_at,
) {
let _ = writeln!(out, "{}{label}", " ".repeat(indent));
}
}
fn long_commands_section(out: &mut String, path: &[&str], meta: &CommandMeta<'_>) {
let mut visible: Vec<&&CommandMeta<'_>> = meta.subcommands.iter().filter(|c| !c.hide).collect();
order_commands(&mut visible);
if visible.is_empty() {
return;
}
let mut lines: Vec<(String, &&CommandMeta<'_>)> = visible
.iter()
.map(|sub| {
let mut sub_path: Vec<&str> = path.to_vec();
sub_path.push(sub.cmd.name);
(usage_line(&sub_path, sub), *sub)
})
.collect();
lines.sort_by(|a, b| {
a.1.display_order
.unwrap_or(999)
.cmp(&b.1.display_order.unwrap_or(999))
.then_with(|| a.0.cmp(&b.0))
});
let default_title = meta.subcommand_help_heading.unwrap_or("Commands");
let mut headings = vec![None];
for (_, sub) in &lines {
let heading = command_help_section(sub, default_title);
if !headings.contains(&heading) {
headings.push(heading);
}
}
for heading in headings {
let title = heading.unwrap_or(default_title);
let _ = writeln!(out, "\n{title}:");
for (usage, sub) in lines
.iter()
.filter(|(_, sub)| command_help_section(sub, default_title) == heading)
{
let _ = write!(out, " {usage}");
let visible_aliases: Vec<&str> = sub
.cmd
.aliases
.iter()
.copied()
.filter(|a| !sub.hidden_aliases.contains(a))
.collect();
if !visible_aliases.is_empty() {
let _ = write!(out, " [aliases: {}]", visible_aliases.join(", "));
}
out.push('\n');
if let Some(about) = sub.long_about.or(sub.about) {
write_indented(out, about.trim_end(), 4);
}
command_deprecation(out, sub, 4);
out.push('\n');
}
if heading.is_none() && !meta.cmd.disable_help_subcommand {
let _ = writeln!(
out,
" help\n Print this message or the help of the given subcommand(s)"
);
}
}
}
fn flat_commands_long(out: &mut String, path: &[&str], meta: &CommandMeta<'_>, width: usize) {
let mut visible: Vec<_> = meta.subcommands.iter().filter(|sub| !sub.hide).collect();
order_commands(&mut visible);
for sub in visible {
let mut sub_path = path.to_vec();
sub_path.push(sub.cmd.name);
let _ = writeln!(out, "\n{}:", sub_path.join(" "));
if let Some(about) = sub
.long_about
.or(sub.about)
.filter(|about| !about.trim().is_empty())
{
let _ = writeln!(out, "{}", about.trim_end());
}
command_deprecation(out, sub, 0);
let mut args: Vec<_> = sub
.args
.iter()
.filter(|arg| !arg.hide && !arg.hide_long_help)
.collect();
order_args(&mut args, sub.args);
let mut flags: Vec<&FlagMeta<'_>> = sub
.flags
.iter()
.filter(|flag| !flag.flag.global && !flag.hide && !flag.hide_long_help)
.collect();
order_flags(&mut flags, sub.flags);
let arg_col = args
.iter()
.map(|arg| arg_usage(arg).chars().count())
.max()
.unwrap_or(0);
let flag_col = flags
.iter()
.map(|flag| column_usage(flag).chars().count())
.max()
.unwrap_or(0);
for arg in args {
entry(
out,
&arg_usage(arg),
arg.long_help.or(arg.help),
arg_col,
width,
meta.next_line_help,
);
long_annotations(
out,
if arg.hide_possible_values {
&[]
} else {
arg.choices
},
if arg.hide_env { None } else { arg.env },
if arg.hide_env { &[] } else { arg.env_fallback },
if arg.hide_env {
&[]
} else {
arg.deprecated_env
},
if arg.hide_default_value {
&[]
} else {
arg.default
},
);
}
for flag in flags {
entry(
out,
&column_usage(flag),
flag.long_help.or(flag.help),
flag_col,
width,
meta.next_line_help,
);
long_annotations(
out,
if flag.hide_possible_values {
&[]
} else {
flag.choices
},
if flag.hide_env { None } else { flag.env },
if flag.hide_env {
&[]
} else {
flag.env_fallback
},
if flag.hide_env {
&[]
} else {
flag.deprecated_env
},
if flag.hide_default_value {
&[]
} else {
flag.default
},
);
flag_notes(out, flag, 4);
}
if sub.flatten_help {
flat_commands_long(out, &sub_path, sub, width);
}
out.push('\n');
}
}
pub fn find<'a>(
spec: &'a 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::Flag;
macro_rules! entry {
($name:ident, $flag:ident, $key:expr, $label:expr, $longs:expr, $shorts:expr, $help:expr) => {
static $flag: Flag<'static> = Flag {
key: $key,
name: $label,
longs: $longs,
shorts: $shorts,
..Flag::BOOL
};
pub static $name: FlagMeta<'static> = FlagMeta {
flag: &$flag,
help: Some($help),
..FlagMeta::EMPTY
};
};
}
entry!(
HELP_BOTH,
HB,
crate::HELP_LONG_KEY,
"help",
&["help"],
b"h",
"Print help"
);
entry!(
HELP_LONG_ONLY,
HL,
crate::HELP_LONG_KEY,
"help",
&["help"],
b"",
"Print help"
);
entry!(
HELP_SHORT_ONLY,
HS,
crate::HELP_SHORT_KEY,
"h",
&[],
b"h",
"Print help"
);
entry!(
VERSION_BOTH,
VB,
crate::VERSION_LONG_KEY,
"version",
&["version"],
b"V",
"Print version"
);
entry!(
VERSION_LONG_ONLY,
VL,
crate::VERSION_LONG_KEY,
"version",
&["version"],
b"",
"Print version"
);
entry!(
VERSION_SHORT_ONLY,
VS,
crate::VERSION_SHORT_KEY,
"V",
&[],
b"V",
"Print version"
);
}
fn supplied_entries(cmd: &Command<'_>, taken: &[String]) -> Vec<&'static FlagMeta<'static>> {
let pick = |long: &str, short: char, both, l, s| match (
taken.contains(&format!("--{long}")),
taken.contains(&format!("-{short}")),
) {
(true, true) => None,
(true, false) => Some(s),
(false, true) => Some(l),
(false, false) => Some(both),
};
let mut out = Vec::new();
if !cmd.disable_help_flag {
out.extend(pick(
"help",
'h',
&supplied::HELP_BOTH,
&supplied::HELP_LONG_ONLY,
&supplied::HELP_SHORT_ONLY,
));
}
if cmd.version && !cmd.disable_version_flag {
out.extend(pick(
"version",
'V',
&supplied::VERSION_BOTH,
&supplied::VERSION_LONG_ONLY,
&supplied::VERSION_SHORT_ONLY,
));
}
out
}
fn own_and_global<'a>(
chain: &[&'a CommandMeta<'a>],
inherit_version_actions: bool,
) -> (Vec<&'a FlagMeta<'a>>, Vec<(&'a FlagMeta<'a>, String)>) {
let Some((here, ancestors)) = chain.split_last() else {
return (Vec::new(), Vec::new());
};
let own: Vec<&FlagMeta<'_>> = here.flags.iter().filter(|f| !f.hide).collect();
fn forms<'f>(f: &'f FlagMeta<'_>) -> impl Iterator<Item = String> + 'f {
f.flag
.longs
.iter()
.map(|l| format!("--{l}"))
.chain(f.flag.shorts.iter().map(|s| format!("-{}", *s as char)))
}
fn negation(f: &FlagMeta<'_>) -> Option<String> {
f.flag.negate.map(|n| format!("--{n}"))
}
let every_form: Vec<String> = here
.flags
.iter()
.chain(ancestors.iter().flat_map(|m| m.flags.iter()).filter(|f| {
f.flag.global || (inherit_version_actions && crate::is_version_flag(f.flag))
}))
.flat_map(forms)
.collect();
let mut taken: Vec<String> = here.flags.iter().flat_map(forms).collect();
let mut taken_negations: Vec<String> = here.flags.iter().filter_map(negation).collect();
let mut keep: Vec<(*const FlagMeta<'_>, Shown<'_>)> = Vec::new();
for meta in ancestors.iter().rev() {
for f in meta.flags.iter().filter(|f| {
f.flag.global || (inherit_version_actions && crate::is_version_flag(f.flag))
}) {
let show = Shown::surviving(f, &taken, &taken_negations, &every_form);
taken.extend(forms(f));
taken_negations.extend(negation(f));
if f.hide || show.nothing() {
continue;
}
keep.push((f as *const _, show));
}
}
let mut inherited: Vec<(&FlagMeta<'_>, String)> = ancestors
.iter()
.flat_map(|meta| meta.flags.iter())
.filter_map(|f| {
keep.iter()
.find(|(p, _)| core::ptr::eq(*p, f as *const _))
.map(|(_, show)| (f, column_usage_masked(f, show)))
})
.collect();
let inherited_positions: Vec<*const FlagMeta<'_>> = inherited
.iter()
.map(|(flag, _)| *flag as *const _)
.collect();
inherited.sort_by_key(|(flag, _)| {
flag.display_order.unwrap_or_else(|| {
inherited_positions
.iter()
.position(|candidate| core::ptr::eq(*candidate, *flag as *const _))
.unwrap_or(usize::MAX)
})
});
let mut own = own;
order_flags(&mut own, here.flags);
let claimed: Vec<String> = taken
.iter()
.cloned()
.chain(taken_negations.iter().cloned())
.collect();
if inherit_version_actions {
if let Some(root) = ancestors.first() {
inherited.extend(
supplied_entries(root.cmd, &claimed)
.into_iter()
.filter(|flag| {
matches!(
flag.flag.key,
crate::VERSION_LONG_KEY | crate::VERSION_SHORT_KEY
)
})
.map(|flag| (flag, column_usage(flag))),
);
}
}
own.extend(supplied_entries(here.cmd, &claimed));
(own, inherited)
}
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)?;
let page = if long {
long_help(spec, &path, &chain)
} else {
short_help(spec, &path, &chain)
};
let (headings, flag_usages, synopsis) = help_structure(spec, &path, &chain, long, false);
Some(styled_help(
&page,
style,
&headings,
&flag_usages,
&synopsis,
))
}
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)?;
let page = if long {
long_help(spec, &path, &chain)
} else {
short_help(spec, &path, &chain)
};
let (headings, flag_usages, synopsis) = help_structure(spec, &path, &chain, long, false);
Some(styled_help(
&page,
style,
&headings,
&flag_usages,
&synopsis,
))
}
pub fn render_view_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
view: &ViewMeta<'_>,
long: bool,
style: Style,
) -> Option<String> {
let (canonical_path, canonical_chain) = route_context(spec, route)?;
let depth = view.root.split_ascii_whitespace().count();
let promoted = *canonical_chain.get(depth)?;
let (root_flags, root_groups) = view_root_fields(spec, promoted, view);
let root_command = Command {
version: spec.root.cmd.version,
disable_version_flag: spec.root.cmd.disable_version_flag,
..*promoted.cmd
};
let root = CommandMeta {
cmd: &root_command,
flags: &root_flags,
groups: &root_groups,
..*promoted
};
let mut chain = Vec::with_capacity(canonical_chain.len());
chain.push(&root);
chain.extend_from_slice(canonical_chain.get(depth + 1..).unwrap_or_default());
let mut path = Vec::with_capacity(canonical_path.len().saturating_sub(depth));
path.push(view.bin);
path.extend_from_slice(canonical_path.get(depth + 1..).unwrap_or_default());
let viewed = Spec {
name: view.name,
bin: Some(view.bin),
about: promoted.about,
long_about: promoted.long_about,
usage: None,
default_subcommand: None,
multicall: false,
root: &root,
..*spec
};
let page = if long {
long_help_with(&viewed, &path, &chain, true)
} else {
short_help_with(&viewed, &path, &chain, true)
};
let (headings, flag_usages, synopsis) = help_structure(&viewed, &path, &chain, long, true);
Some(styled_help(
&page,
style,
&headings,
&flag_usages,
&synopsis,
))
}
pub fn render_all_at(spec: &Spec<'_>, route: &[&Command<'_>]) -> Option<String> {
render_all_at_styled(spec, route, Style::PLAIN)
}
pub fn render_all_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
style: Style,
) -> Option<String> {
let (path, chain) = route_context(spec, route)?;
Some(recursive_help(spec, path, chain, style, false))
}
pub fn render_all_view_at_styled(
spec: &Spec<'_>,
route: &[&Command<'_>],
view: &ViewMeta<'_>,
style: Style,
) -> Option<String> {
let (canonical_path, canonical_chain) = route_context(spec, route)?;
let depth = view.root.split_ascii_whitespace().count();
let promoted = *canonical_chain.get(depth)?;
let (root_flags, root_groups) = view_root_fields(spec, promoted, view);
let root_command = Command {
version: spec.root.cmd.version,
disable_version_flag: spec.root.cmd.disable_version_flag,
..*promoted.cmd
};
let root = CommandMeta {
cmd: &root_command,
flags: &root_flags,
groups: &root_groups,
..*promoted
};
let mut chain = Vec::with_capacity(canonical_chain.len().saturating_sub(depth));
chain.push(&root);
chain.extend_from_slice(canonical_chain.get(depth + 1..).unwrap_or_default());
let mut path = Vec::with_capacity(canonical_path.len().saturating_sub(depth));
path.push(view.bin);
path.extend_from_slice(canonical_path.get(depth + 1..).unwrap_or_default());
let viewed = Spec {
name: view.name,
bin: Some(view.bin),
about: promoted.about,
long_about: promoted.long_about,
usage: None,
default_subcommand: None,
multicall: false,
root: &root,
..*spec
};
Some(recursive_help(&viewed, path, chain, style, true))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
Short,
Long,
All,
}
pub fn page(
spec: &Spec<'_>,
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
cmd: &Command<'_>,
page: Page,
style: Style,
) -> Option<String> {
match route_to(root, argv, cmd) {
Some(route) => match page {
Page::Short => render_at_styled(spec, &route, false, style),
Page::Long => render_at_styled(spec, &route, true, style),
Page::All => render_all_at_styled(spec, &route, style),
},
None => match page {
Page::Short => render_styled(spec, cmd, false, style),
Page::Long => render_styled(spec, cmd, true, style),
Page::All => render_all_styled(spec, cmd, style),
},
}
}
pub fn page_view(
spec: &Spec<'_>,
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
cmd: &Command<'_>,
view: &ViewMeta<'_>,
page: Page,
style: Style,
) -> Option<String> {
match route_to_view(root, argv, cmd, view) {
Some(route) => match page {
Page::Short => render_view_at_styled(spec, &route, view, false, style),
Page::Long => render_view_at_styled(spec, &route, view, true, style),
Page::All => render_all_view_at_styled(spec, &route, view, style),
},
None => match page {
Page::Short => render_styled(spec, cmd, false, style),
Page::Long => render_styled(spec, cmd, true, style),
Page::All => render_all_styled(spec, cmd, style),
},
}
}
pub(crate) fn view_root_flags<'a>(
spec: &'a Spec<'a>,
promoted: &CommandMeta<'a>,
view: &ViewMeta<'a>,
) -> Vec<FlagMeta<'a>> {
let selected = |flag: &&FlagMeta<'a>| {
let carried = crate::is_version_flag(flag.flag)
|| (flag.flag.global
&& (view.all_globals
|| view.globals.iter().any(|selector| {
selector
.strip_prefix("--")
.is_some_and(|long| flag.flag.longs.contains(&long))
|| selector
.strip_prefix('-')
.filter(|short| short.len() == 1)
.and_then(|short| short.as_bytes().first().copied())
.is_some_and(|short| flag.flag.shorts.contains(&short))
})));
carried
&& !promoted
.flags
.iter()
.any(|local| crate::spec::flag_forms_overlap(flag.flag, local.flag))
};
let mut flags: Vec<FlagMeta<'a>> = spec.root.flags.iter().filter(selected).copied().collect();
flags.extend_from_slice(promoted.flags);
flags
}
pub(crate) fn view_root_fields<'a>(
spec: &'a Spec<'a>,
promoted: &CommandMeta<'a>,
view: &ViewMeta<'a>,
) -> (Vec<FlagMeta<'a>>, Vec<crate::spec::GroupMeta<'a>>) {
let mut flags = view_root_flags(spec, promoted, view);
let carried = flags.len().saturating_sub(promoted.flags.len());
let matches = |flag: &FlagMeta<'_>, selector: &str| {
selector
.strip_prefix("--")
.is_some_and(|long| flag.flag.longs.contains(&long))
|| selector
.strip_prefix('-')
.filter(|short| short.len() == 1)
.and_then(|short| short.as_bytes().first().copied())
.is_some_and(|short| flag.flag.shorts.contains(&short))
};
let mut groups = Vec::new();
for group in spec.root.groups {
let members: Vec<usize> = group
.members
.iter()
.filter_map(|selector| {
flags[..carried]
.iter()
.position(|flag| matches(flag, selector))
})
.collect();
match members.as_slice() {
[only] if group.required => flags[*only].required = true,
[_, _, ..] => {
groups.push(*group);
}
_ => {}
}
}
groups.extend_from_slice(promoted.groups);
(flags, groups)
}
fn recursive_help<'a>(
spec: &'a Spec<'a>,
path: Vec<&'a str>,
chain: Vec<&'a CommandMeta<'a>>,
style: Style,
inherit_version_actions: bool,
) -> String {
fn append<'a>(
out: &mut String,
spec: &'a Spec<'a>,
path: &mut Vec<&'a str>,
chain: &mut Vec<&'a CommandMeta<'a>>,
style: Style,
inherit_version_actions: bool,
) {
if !out.is_empty() {
out.push('\n');
}
let page = long_help_with(spec, path, chain, inherit_version_actions);
let (headings, flag_usages, synopsis) =
help_structure(spec, path, chain, true, inherit_version_actions);
out.push_str(&styled_help(
&page,
style,
&headings,
&flag_usages,
&synopsis,
));
let current = *chain.last().expect("a recursive page has a command");
let mut children: Vec<_> = current.subcommands.iter().filter(|cmd| !cmd.hide).collect();
children.sort_by_key(|cmd| (cmd.display_order.unwrap_or(999), cmd.cmd.name));
for child in children {
path.push(child.cmd.name);
chain.push(child);
append(out, spec, path, chain, style, inherit_version_actions);
chain.pop();
path.pop();
}
}
let mut out = String::new();
let mut path = path;
let mut chain = chain;
append(
&mut out,
spec,
&mut path,
&mut chain,
style,
inherit_version_actions,
);
out
}
#[cfg(test)]
mod style_tests {
use super::{
commands_section, display_usage_masked, flag_notes, flag_usage, flat_commands_short,
inline_environment_notes, long_help, render_view_at_styled, styled_flag_usage, styled_help,
Shown, Style,
};
use crate::spec::{CommandMeta, FlagMeta, Spec, ViewMeta};
use crate::{ArgAction, Command, Flag};
#[test]
fn optional_equals_values_put_the_equals_inside_the_brackets() {
let flag = Flag {
name: "color",
longs: &["color"],
require_equals: true,
..Flag::VALUE
};
let meta = FlagMeta {
flag: &flag,
value_name: Some("WHEN"),
value_optional: true,
..FlagMeta::EMPTY
};
assert_eq!(flag_usage(&meta), "--color[=WHEN]");
}
#[test]
fn a_negation_left_after_positive_spellings_are_masked_keeps_its_flag_name() {
let flag = Flag {
name: "color",
shorts: b"c",
longs: &["color"],
negate: Some("no-color"),
..Flag::BOOL
};
let meta = FlagMeta {
flag: &flag,
..FlagMeta::EMPTY
};
let shown = Shown {
long: None,
short: None,
negate: true,
};
assert_eq!(display_usage_masked(&meta, &shown), "color: --no-color");
}
#[test]
fn a_flag_spelled_only_as_its_negation_writes_that_spelling_and_nothing_before_it() {
let flag = Flag {
name: "no-credit",
negate: Some("no-credit"),
..Flag::BOOL
};
let meta = FlagMeta {
flag: &flag,
..FlagMeta::EMPTY
};
let shown = Shown {
long: None,
short: None,
negate: true,
};
assert_eq!(display_usage_masked(&meta, &shown), "--no-credit");
}
#[test]
fn flattened_next_line_deprecation_follows_help_without_a_blank_row() {
let flag = Flag {
name: "old",
longs: &["old"],
..Flag::BOOL
};
let flag_meta = FlagMeta {
flag: &flag,
help: Some("Use the old mode"),
deprecated: Some("use --new"),
..FlagMeta::EMPTY
};
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
flags: &[flag_meta],
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
next_line_help: true,
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();
flat_commands_short(&mut page, &["tool"], &root_meta);
assert!(
page.contains(" Use the old mode\n [deprecated: use --new]"),
"{page}"
);
assert!(!page.contains("Use the old mode\n\n [deprecated"));
}
#[test]
fn flattened_next_line_flags_without_help_still_end_their_usage_rows() {
let old = Flag {
name: "old",
longs: &["old"],
..Flag::BOOL
};
let new = Flag {
name: "new",
longs: &["new"],
..Flag::BOOL
};
let flags = [
FlagMeta {
flag: &old,
deprecated: Some("use --new"),
..FlagMeta::EMPTY
},
FlagMeta {
flag: &new,
..FlagMeta::EMPTY
},
];
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
flags: &flags,
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
next_line_help: true,
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();
flat_commands_short(&mut page, &["tool"], &root_meta);
assert!(page.contains("--old\n"), "{page}");
assert!(page.contains("[deprecated: use --new]\n"), "{page}");
assert!(page.contains("--new\n"), "{page}");
assert!(!page.contains("--old [deprecated"), "{page}");
}
#[test]
fn hidden_environment_names_include_fallbacks_and_deprecated_aliases() {
let flag = Flag {
name: "token",
..Flag::BOOL
};
let meta = FlagMeta {
flag: &flag,
hide_env: true,
env_fallback: &["OLD_TOKEN"],
deprecated_env: &["LEGACY_TOKEN"],
..FlagMeta::EMPTY
};
let mut page = String::new();
flag_notes(&mut page, &meta, 4);
assert!(page.is_empty());
let visible = inline_environment_notes(false, &["OLD_TOKEN"], &["LEGACY_TOKEN"])
.expect("visible environment notes");
assert!(visible.contains("[env fallback: OLD_TOKEN]"));
assert!(visible.contains("[deprecated env: LEGACY_TOKEN]"));
assert!(inline_environment_notes(true, &["OLD_TOKEN"], &["LEGACY_TOKEN"]).is_none());
}
#[test]
fn short_command_rows_trim_trailing_help_whitespace() {
let sub_cmd = Command {
name: "run",
..Command::EMPTY
};
let sub_meta = CommandMeta {
cmd: &sub_cmd,
about: Some("run it\n"),
..CommandMeta::EMPTY
};
let subcommands = [&sub_meta];
let root_meta = CommandMeta {
subcommands: &subcommands,
..CommandMeta::EMPTY
};
let mut page = String::new();
commands_section(&mut page, &[], &root_meta);
assert!(page.contains(" run run it\n help"));
assert!(!page.contains(" run run it\n\n help"));
}
#[test]
fn long_help_preserves_configured_spacing_before_package_metadata() {
let command = Command {
name: "ex",
..Command::EMPTY
};
let root = CommandMeta {
cmd: &command,
after_help: Some("More help.\n"),
..CommandMeta::EMPTY
};
let spec = Spec {
name: "ex",
author: Some("Example Author"),
root: &root,
..Spec::EMPTY
};
let page = long_help(&spec, &["ex"], &[&root]);
assert!(
page.contains("More help.\n\n\nAuthor: Example Author\n"),
"{page}"
);
}
#[test]
fn view_help_keeps_declared_and_synthesized_host_version_actions() {
let build_info = Flag {
name: "build-info",
longs: &["build-info"],
action: ArgAction::Version,
..Flag::BOOL
};
let nested_command = Command {
name: "status",
..Command::EMPTY
};
let child_command = Command {
name: "serve",
subcommands: &[&nested_command],
..Command::EMPTY
};
let root_command = Command {
name: "host",
flags: &[&build_info],
subcommands: &[&child_command],
version: true,
..Command::EMPTY
};
let build_info_meta = FlagMeta {
flag: &build_info,
help: Some("Print build information"),
..FlagMeta::EMPTY
};
let nested_meta = CommandMeta {
cmd: &nested_command,
..CommandMeta::EMPTY
};
let child_meta = CommandMeta {
cmd: &child_command,
subcommands: &[&nested_meta],
..CommandMeta::EMPTY
};
let root_meta = CommandMeta {
cmd: &root_command,
flags: &[build_info_meta],
subcommands: &[&child_meta],
..CommandMeta::EMPTY
};
let spec = Spec {
name: "host",
bin: Some("host"),
root: &root_meta,
..Spec::EMPTY
};
let view = ViewMeta {
id: "server",
name: "server",
bin: "server",
root: "serve",
all_globals: false,
globals: &[],
};
let page = render_view_at_styled(
&spec,
&[&root_command, &child_command, &nested_command],
&view,
false,
Style::PLAIN,
)
.expect("view route");
assert!(page.contains("--build-info"), "{page}");
assert!(page.contains("-V, --version"), "{page}");
}
#[test]
fn coloured_help_styles_structure_without_changing_plain_text() {
let page = "A summary ending in:\nUsage: prose is not a synopsis\nExamples:\n\nUsage: ex [OPTIONS]\n ex --all\n\nOptions:\n -f, --force Force it\n [possible values: --auto]\n (default: -1)\n";
let headings = vec!["Options".to_string()];
let usages = vec!["-f, --force".to_string()];
let synopsis = vec![
"Usage: ex [OPTIONS]".to_string(),
" ex --all".to_string(),
];
assert_eq!(
styled_help(page, Style::PLAIN, &headings, &usages, &synopsis),
page
);
let coloured = styled_help(page, Style::COLOURED, &headings, &usages, &synopsis);
assert!(coloured.contains("\u{1b}[1;4;32mUsage:\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[1;4;32mOptions:\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[36m-f\u{1b}[0m"));
assert!(coloured.contains("\u{1b}[36m--force\u{1b}[0m"));
assert!(coloured.contains("A summary ending in:\nUsage: prose is not a synopsis"));
assert!(coloured.contains("Usage: prose is not a synopsis\nExamples:"));
assert!(coloured.contains("\u{1b}[36m ex --all\u{1b}[0m"));
assert!(coloured.contains("[possible values: --auto]"));
assert!(coloured.contains("(default: -1)"));
assert_eq!(strip_ansi(&coloured), page);
}
#[test]
fn equals_separates_a_coloured_flag_from_its_value() {
assert_eq!(
styled_flag_usage("--output=<FILE>", Style::COLOURED),
"\u{1b}[36m--output\u{1b}[0m=<FILE>"
);
assert_eq!(
styled_flag_usage("--color[=WHEN]", Style::COLOURED),
"\u{1b}[36m--color\u{1b}[0m[=WHEN]"
);
}
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
}
}