use crate::spec::{CommandMeta, FlagMeta, Spec, ViewMeta};
use crate::{Command, DoubleDash, Error};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Code {
UnknownFlag,
MissingFlagValue,
UnexpectedArg,
ArgRequiresDoubleDash,
SubcommandConflict,
TooDeep,
MissingRequired,
DuplicateFlag,
InvalidChoice,
VarTooFew,
VarTooMany,
ConflictingFlags,
InvalidValue,
MissingGroup,
MissingSubcommand,
Help,
MissingArgsHelp,
HelpAll,
Version,
}
impl Code {
pub const fn as_str(self) -> &'static str {
match self {
Self::UnknownFlag => "unknown_flag",
Self::MissingFlagValue => "missing_flag_value",
Self::UnexpectedArg => "unexpected_arg",
Self::ArgRequiresDoubleDash => "arg_requires_double_dash",
Self::SubcommandConflict => "subcommand_conflict",
Self::TooDeep => "too_deep",
Self::MissingRequired => "missing_required",
Self::DuplicateFlag => "duplicate_flag",
Self::InvalidChoice => "invalid_choice",
Self::VarTooFew => "var_too_few",
Self::VarTooMany => "var_too_many",
Self::ConflictingFlags => "conflicting_flags",
Self::InvalidValue => "invalid_value",
Self::MissingGroup => "missing_group",
Self::MissingSubcommand => "missing_subcommand",
Self::Help => "help",
Self::MissingArgsHelp => "missing_args_help",
Self::HelpAll => "help_all",
Self::Version => "version",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArgvSpan {
pub index: usize,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub code: Code,
pub subject: Option<String>,
pub location: Option<ArgvSpan>,
pub rendered: String,
}
#[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 _;
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 {
return Style::PLAIN;
}
if forced || std::io::stderr().is_terminal() {
Style::COLOURED
} else {
Style::PLAIN
}
}
fn put(self, out: &mut String, ink: Ink, text: &str) {
self.put_joined(out, ink, &[text]);
}
#[inline(never)]
fn put_joined(self, out: &mut String, ink: Ink, texts: &[&str]) {
let code = match ink {
Ink::Plain => None,
Ink::Error => Some("1m\u{1b}[31"),
Ink::Warning => Some("1m\u{1b}[33"),
Ink::Invalid => Some("33"),
Ink::Valid => Some("32"),
Ink::Heading => Some("1m\u{1b}[4"),
Ink::Literal => Some("1"),
};
match code.filter(|_| self.coloured) {
Some(code) => {
push_all(out, &["\u{1b}[", code, "m"]);
push_all(out, texts);
push_all(out, &["\u{1b}[0m"]);
}
None => push_all(out, texts),
}
}
#[inline(never)]
fn put_all(self, out: &mut String, parts: &[(Ink, &str)]) {
for (ink, text) in parts {
self.put(out, *ink, text);
}
}
}
#[inline(never)]
fn push_all(out: &mut String, texts: &[&str]) {
for text in texts {
out.push_str(text);
}
}
#[derive(Clone, Copy)]
enum Ink {
Plain,
Error,
Warning,
Invalid,
Valid,
Heading,
Literal,
}
fn flag_named(token: &str) -> &str {
match token.strip_prefix("--") {
Some(body) => match body.find('=') {
Some(i) => &token[..i + 2],
None => token,
},
None => token,
}
}
fn long_names_in_scope<'a>(chain: &[&'a CommandMeta<'a>]) -> Vec<&'a str> {
let mut names = Vec::new();
for (i, meta) in chain.iter().enumerate() {
let own = i + 1 == chain.len();
for f in meta.flags {
if !f.hide && (own || f.flag.global) {
names.extend_from_slice(f.flag.longs);
names.extend(f.flag.negate);
}
}
}
names
}
fn jaro(a: &str, b: &str) -> f64 {
let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
if a.is_empty() && b.is_empty() {
return 1.0;
}
if a.is_empty() || b.is_empty() {
return 0.0;
}
let window = (a.len().max(b.len()) / 2).saturating_sub(1);
let mut a_matched = vec![false; a.len()];
let mut b_matched = vec![false; b.len()];
let mut matches = 0usize;
for (i, ch) in a.iter().enumerate() {
let start = i.saturating_sub(window);
let end = (i + window + 1).min(b.len());
for j in start..end {
if !b_matched[j] && b[j] == *ch {
a_matched[i] = true;
b_matched[j] = true;
matches += 1;
break;
}
}
}
if matches == 0 {
return 0.0;
}
let mut transpositions = 0usize;
let mut k = 0usize;
for (i, matched) in a_matched.iter().enumerate() {
if !matched {
continue;
}
while !b_matched[k] {
k += 1;
}
if a[i] != b[k] {
transpositions += 1;
}
k += 1;
}
let matches = matches as f64;
(matches / a.len() as f64
+ matches / b.len() as f64
+ (matches - transpositions as f64 / 2.0) / matches)
/ 3.0
}
fn nearest<'a>(typed: &str, candidates: &[&'a str]) -> Vec<&'a str> {
let mut scored: Vec<(f64, &str)> = Vec::new();
for candidate in candidates {
let score = jaro(typed, candidate);
if score > 0.7 {
scored.push((score, candidate));
}
}
crate::order::sort_by(&mut scored, &mut |a, b| {
a.0.total_cmp(&b.0).then_with(|| a.1.cmp(b.1))
});
scored.dedup_by(|a, b| a.1 == b.1);
scored.into_iter().map(|(_, candidate)| candidate).collect()
}
fn tip(out: &mut String, style: Style, noun: &str, prefix: &str, near: &[&str]) {
if near.is_empty() {
return;
}
let one = near.len() == 1;
style.put_all(
out,
&[
(Ink::Plain, "\n "),
(Ink::Valid, "tip:"),
(
Ink::Plain,
if one { " a similar " } else { " some similar " },
),
(Ink::Plain, noun),
(Ink::Plain, if one { " exists: " } else { "s exist: " }),
],
);
for (i, candidate) in near.iter().enumerate() {
push_all(out, &[if i == 0 { "'" } else { "', '" }]);
style.put_joined(out, Ink::Valid, &[prefix, candidate]);
}
push_all(out, &["'\n"]);
}
fn separator_before(argv: &[&std::ffi::OsStr], token: &[u8]) -> bool {
let Some(limit) = span_of_slice(argv, token)
.map(|span| span.index)
.or_else(|| {
argv.iter()
.position(|word| word.as_encoded_bytes() == token)
})
else {
return false;
};
argv.iter()
.take(limit)
.any(|word| word.as_encoded_bytes() == b"--")
}
#[derive(Clone, Copy, Default)]
struct ValueCapture {
takes_positionals: bool,
captures_trailing: bool,
separator_seen: bool,
}
fn value_capture(
argv: &[&std::ffi::OsStr],
token: &[u8],
here: Option<&CommandMeta<'_>>,
) -> ValueCapture {
let Some(meta) = here else {
return ValueCapture::default();
};
let clause_args = meta.cmd.clause.map(|clause| clause.args).unwrap_or(&[]);
let positionals = || meta.cmd.args.iter().chain(clause_args.iter());
ValueCapture {
takes_positionals: positionals().next().is_some(),
captures_trailing: positionals().any(|arg| {
matches!(
arg.double_dash,
DoubleDash::Required | DoubleDash::Automatic
)
}),
separator_seen: separator_before(argv, token),
}
}
#[inline(never)]
fn unexpected_flag(
out: &mut String,
style: Style,
typed: &str,
chain: &[&CommandMeta<'_>],
capture: ValueCapture,
) {
use Ink::{Invalid, Plain, Valid};
error_line(
out,
style,
&[
(Plain, "unexpected argument '"),
(Invalid, typed),
(Plain, "' found"),
],
);
let bare = typed.trim_start_matches('-');
let near = nearest(bare, &long_names_in_scope(chain));
tip(out, style, "argument", "--", &near);
let as_value = capture.takes_positionals
&& !capture.separator_seen
&& (near.is_empty() || capture.captures_trailing);
if as_value {
style.put_all(
out,
&[
(Plain, if near.is_empty() { "\n " } else { " " }),
(Valid, "tip:"),
(Plain, " to pass '"),
(Invalid, typed),
(Plain, "' as a value, use '"),
],
);
style.put_joined(out, Valid, &["-- ", typed]);
push_all(out, &["'\n"]);
}
}
fn shown<'a>(meta: Option<&'a CommandMeta<'a>>, name: &str) -> String {
let Some(meta) = meta else {
return name.to_string();
};
if let Some(arg) = meta.args.iter().find(|a| a.arg.name == name) {
return crate::help::arg_usage(arg);
}
if let Some(flag) = meta
.flags
.iter()
.find(|f| f.flag.name == name || f.value_name == Some(name))
{
return crate::help::flag_spelling(flag);
}
name.to_string()
}
fn group_member_shown(meta: Option<&CommandMeta<'_>>, selector: &str) -> String {
let Some(meta) = meta else {
return selector.to_string();
};
let long = selector.strip_prefix("--");
let short = selector.strip_prefix('-').and_then(|rest| {
let mut chars = rest.chars();
let only = u8::try_from(chars.next()?).ok()?;
chars.next().is_none().then_some(only)
});
let found = meta.flags.iter().find(|flag| {
long.is_some_and(|long| flag.flag.longs.contains(&long) || flag.flag.negate == Some(long))
|| short.is_some_and(|short| flag.flag.shorts.contains(&short))
});
let Some(flag) = found else {
return selector.to_string();
};
let mut shown = crate::help::flag_spelling(flag);
if flag.flag.takes_value {
push_all(
&mut shown,
&[
" <",
flag.value_name.unwrap_or(flag.flag.name),
if flag.flag.variadic { ">…" } else { ">" },
],
);
}
shown
}
fn value_bound_to(
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
name: &str,
refused: &[&str],
view: Option<&ViewMeta<'_>>,
) -> Option<String> {
let mut parser = crate::Parser::new(root, argv);
if let Some(view) = view {
parser = parser.with_view(view);
}
let mut last = None;
while let Some(event) = parser.next_event() {
let value = match event {
Ok(crate::Event::Arg { arg, value, .. }) if arg.name == name => value,
Ok(crate::Event::Flag {
flag,
value: Some(value),
..
}) if flag.name == name => value,
Ok(_) => continue,
Err(_) => break,
};
let value = String::from_utf8_lossy(value).into_owned();
if !refused.is_empty() && !refused.contains(&value.as_str()) {
return Some(value);
}
last = Some(value);
}
last
}
fn path_taken<'t>(
root: &'t Command<'t>,
argv: &[&std::ffi::OsStr],
view: Option<&'t ViewMeta<'t>>,
) -> Vec<&'t Command<'t>> {
let mut path = vec![root];
let mut parser = crate::Parser::new(root, argv);
if let Some(view) = view {
parser = parser.with_view(view);
}
while let Some(event) = parser.next_event() {
match event {
Ok(crate::Event::Command(cmd)) => path.push(cmd),
Ok(_) => {}
Err(_) => break,
}
}
path
}
fn resolve<'a>(
spec: &'a Spec<'a>,
path: &[&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 path.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))
}
fn span_of_slice(argv: &[&std::ffi::OsStr], slice: &[u8]) -> Option<ArgvSpan> {
let start = slice.as_ptr() as usize;
let end = start.checked_add(slice.len())?;
argv.iter().enumerate().find_map(|(index, word)| {
let bytes = word.as_encoded_bytes();
let word_start = bytes.as_ptr() as usize;
let word_end = word_start.checked_add(bytes.len())?;
(start >= word_start && end <= word_end).then(|| ArgvSpan {
index,
start: start - word_start,
end: end - word_start,
})
})
}
fn value_span(
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
name: &str,
wanted: impl Fn(&str) -> bool,
view: Option<&ViewMeta<'_>>,
) -> Option<ArgvSpan> {
let mut parser = crate::Parser::new(root, argv);
if let Some(view) = view {
parser = parser.with_view(view);
}
while let Some(event) = parser.next_event() {
let value = match event {
Ok(crate::Event::Arg { arg, value, .. }) if arg.name == name => value,
Ok(crate::Event::Flag {
flag,
value: Some(value),
..
}) if flag.name == name => value,
Ok(_) => continue,
Err(_) => break,
};
if wanted(&String::from_utf8_lossy(value)) {
return span_of_slice(argv, value);
}
}
None
}
fn flag_span(
root: &Command<'_>,
argv: &[&std::ffi::OsStr],
flag: &crate::Flag<'_>,
view: Option<&ViewMeta<'_>>,
) -> Option<ArgvSpan> {
let mut parser = crate::Parser::new(root, argv);
if let Some(view) = view {
parser = parser.with_view(view);
}
while let Some(event) = parser.next_event() {
let Err(Error::MissingFlagValue { flag: missing }) = event else {
continue;
};
if !core::ptr::eq(missing, flag) {
return None;
}
let index = parser.pos.checked_sub(1)?;
let bytes = argv.get(index)?.as_encoded_bytes();
if let Some(long) = bytes.strip_prefix(b"--") {
let name = long.split(|byte| *byte == b'=').next().unwrap_or(long);
return flag.longs.iter().find_map(|candidate| {
(name == candidate.as_bytes()).then_some(ArgvSpan {
index,
start: 0,
end: candidate.len() + 2,
})
});
}
let shorts = bytes.strip_prefix(b"-")?;
return shorts.iter().enumerate().find_map(|(offset, short)| {
flag.shorts.contains(short).then_some(ArgvSpan {
index,
start: offset + 1,
end: offset + 2,
})
});
}
None
}
fn code(error: &Error<'_, '_>) -> Code {
match error {
Error::UnknownFlag { .. } => Code::UnknownFlag,
Error::MissingFlagValue { .. } => Code::MissingFlagValue,
Error::UnexpectedArg { .. } => Code::UnexpectedArg,
Error::ArgRequiresDoubleDash { .. } => Code::ArgRequiresDoubleDash,
Error::SubcommandConflict { .. } => Code::SubcommandConflict,
Error::TooDeep => Code::TooDeep,
Error::MissingRequired { .. } => Code::MissingRequired,
Error::DuplicateFlag { .. } => Code::DuplicateFlag,
Error::InvalidChoice { .. } => Code::InvalidChoice,
Error::VarTooFew { .. } => Code::VarTooFew,
Error::VarTooMany { .. } => Code::VarTooMany,
Error::ConflictingFlags { .. } => Code::ConflictingFlags,
Error::InvalidValue(_) => Code::InvalidValue,
Error::MissingGroup { .. } => Code::MissingGroup,
Error::MissingSubcommand => Code::MissingSubcommand,
Error::Help { .. } => Code::Help,
Error::MissingArgsHelp { .. } => Code::MissingArgsHelp,
Error::HelpAll { .. } => Code::HelpAll,
Error::Version { .. } => Code::Version,
}
}
fn subject(error: &Error<'_, '_>) -> Option<String> {
match error {
Error::UnknownFlag { token } | Error::UnexpectedArg { token } => {
Some(String::from_utf8_lossy(token).into_owned())
}
Error::MissingFlagValue { flag } => Some(flag.name.to_string()),
Error::ArgRequiresDoubleDash { arg } => Some(arg.name.to_string()),
Error::SubcommandConflict { subcommand } => Some(subcommand.name.to_string()),
Error::MissingRequired { name }
| Error::DuplicateFlag { name }
| Error::InvalidChoice { name, .. }
| Error::VarTooFew { name, .. }
| Error::VarTooMany { name, .. }
| Error::ConflictingFlags { name, .. } => Some((*name).to_string()),
Error::InvalidValue(invalid) => Some(invalid.name.to_string()),
Error::MissingGroup { group, .. } => Some((*group).to_string()),
Error::TooDeep
| Error::MissingSubcommand
| Error::Help { .. }
| Error::MissingArgsHelp { .. }
| Error::HelpAll { .. }
| Error::Version { .. } => None,
}
}
fn location(
spec: &Spec<'_>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
view: Option<&ViewMeta<'_>>,
) -> Option<ArgvSpan> {
match error {
Error::UnknownFlag { token } | Error::UnexpectedArg { token } => span_of_slice(argv, token),
Error::MissingFlagValue { flag } => flag_span(spec.root.cmd, argv, flag, view),
Error::InvalidChoice { name, choices } => value_span(
spec.root.cmd,
argv,
name,
|value| !choices.contains(&value),
view,
),
Error::InvalidValue(invalid) => value_span(
spec.root.cmd,
argv,
invalid.name,
|value| value == invalid.value,
view,
),
_ => None,
}
}
pub fn report(spec: &Spec<'_>, argv: &[&std::ffi::OsStr], error: &Error<'_, '_>) -> Report {
Report {
code: code(error),
subject: subject(error),
location: location(spec, argv, error, None),
rendered: render(spec, argv, error, Style::PLAIN),
}
}
fn view_argv<'a>(
argv: &'a [&'a std::ffi::OsStr],
view: &'a ViewMeta<'_>,
) -> (Vec<&'a std::ffi::OsStr>, usize) {
let words = argv.get(1..).unwrap_or_default();
let root_depth = view.root.split_ascii_whitespace().count();
let mut rewritten = Vec::with_capacity(words.len() + root_depth);
rewritten.extend(view.root.split_ascii_whitespace().map(std::ffi::OsStr::new));
rewritten.extend_from_slice(words);
(rewritten, root_depth)
}
pub fn report_view<'a>(
spec: &'a Spec<'a>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
view: &'a ViewMeta<'a>,
) -> Report {
let (rewritten, root_depth) = view_argv(argv, view);
let location = location(spec, &rewritten, error, Some(view)).and_then(|span| {
(span.index >= root_depth).then_some(ArgvSpan {
index: span.index - root_depth + 1,
start: span.start,
end: span.end,
})
});
Report {
code: code(error),
subject: subject(error),
location,
rendered: render_projected(spec, &rewritten, error, Style::PLAIN, view),
}
}
pub fn render(
spec: &Spec<'_>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
style: Style,
) -> String {
render_plain(spec, argv, error, style)
}
pub fn render_warnings(warnings: &[crate::warn::Warning<'_>], style: Style) -> String {
let mut out = String::new();
for warning in warnings {
style.put_all(
&mut out,
&[
(Ink::Warning, "warning:"),
(Ink::Plain, " "),
(Ink::Invalid, &crate::warn::subject(warning)),
(Ink::Plain, " is deprecated"),
],
);
if let Some(at) = warning.remove_at {
style.put_all(
&mut out,
&[(Ink::Plain, ", removed at "), (Ink::Literal, at)],
);
}
match crate::warn::tail(warning) {
Some(crate::warn::Tail::Message(message)) => push_all(&mut out, &[": ", message]),
Some(crate::warn::Tail::Replacement(replacement)) => style.put_all(
&mut out,
&[(Ink::Plain, ": use "), (Ink::Valid, replacement)],
),
None => {}
}
push_all(&mut out, &["\n"]);
}
out
}
pub fn render_view<'a>(
spec: &'a Spec<'a>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
style: Style,
view: &'a ViewMeta<'a>,
) -> String {
let (rewritten, _) = view_argv(argv, view);
render_projected(spec, &rewritten, error, style, view)
}
fn render_plain(
spec: &Spec<'_>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
style: Style,
) -> String {
let taken = path_taken(spec.root.cmd, argv, None);
let cmd = *taken.last().expect("the root is always on the path");
let resolved = resolve(spec, &taken);
let resolved = resolved
.as_ref()
.map(|(names, chain)| (&names[..], &chain[..]));
describe(spec, spec.root.cmd, argv, error, style, None, cmd, resolved)
}
fn render_projected<'a>(
spec: &'a Spec<'a>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
style: Style,
view: &'a ViewMeta<'a>,
) -> String {
let canonical_spec = spec;
let taken = path_taken(spec.root.cmd, argv, Some(view));
let cmd = *taken.last().expect("the root is always on the path");
let mut resolved = resolve(spec, &taken);
let mut projected_spec = None;
let mut projected_flags: Vec<FlagMeta<'_>> = Vec::new();
let mut projected_groups = Vec::new();
let depth = view.root.split_ascii_whitespace().count();
let promoted = resolved
.as_ref()
.and_then(|(_, chain)| chain.get(depth).copied());
if let Some(promoted) = promoted {
let (flags, groups) = crate::help::view_root_fields(spec, promoted, view);
projected_flags = flags;
projected_groups = groups;
}
let projected_root = promoted.map(|promoted| CommandMeta {
flags: &projected_flags,
groups: &projected_groups,
..*promoted
});
if let (Some(promoted), Some(root), Some((names, chain))) =
(promoted, projected_root.as_ref(), resolved.as_mut())
{
chain[0] = root;
chain.drain(1..=depth.min(chain.len().saturating_sub(1)));
names.drain(1..=depth.min(names.len().saturating_sub(1)));
names[0] = view.bin;
projected_spec = Some(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,
..*spec
});
}
let spec = projected_spec.as_ref().unwrap_or(spec);
let resolved = resolved
.as_ref()
.map(|(names, chain)| (&names[..], &chain[..]));
describe(
spec,
canonical_spec.root.cmd,
argv,
error,
style,
Some(view),
cmd,
resolved,
)
}
#[inline(never)]
fn error_line(out: &mut String, style: Style, parts: &[(Ink, &str)]) {
style.put_all(out, &[(Ink::Error, "error:"), (Ink::Plain, " ")]);
style.put_all(out, parts);
push_all(out, &["\n"]);
}
#[inline(never)]
fn listed_line(out: &mut String, style: Style, text: &str) {
style.put_all(
out,
&[(Ink::Plain, " "), (Ink::Valid, text), (Ink::Plain, "\n")],
);
}
#[allow(clippy::too_many_arguments)]
fn describe(
spec: &Spec<'_>,
canonical_root: &Command<'_>,
argv: &[&std::ffi::OsStr],
error: &Error<'_, '_>,
style: Style,
view: Option<&ViewMeta<'_>>,
cmd: &Command<'_>,
resolved: Option<(&[&str], &[&CommandMeta<'_>])>,
) -> String {
use Ink::{Invalid, Literal, Plain};
let chain: &[&CommandMeta<'_>] = resolved.map(|(_, c)| c).unwrap_or(&[]);
let here = chain.last().copied();
let path = || {
resolved
.map(|(names, _)| names.join(" "))
.unwrap_or_else(|| spec.bin.unwrap_or(spec.name).to_string())
};
let named = match error {
Error::MissingRequired { name }
| Error::DuplicateFlag { name }
| Error::InvalidChoice { name, .. }
| Error::ConflictingFlags { name, .. }
| Error::VarTooFew { name, .. }
| Error::VarTooMany { name, .. } => shown(here, name),
Error::InvalidValue(invalid) => shown(here, invalid.name),
Error::ArgRequiresDoubleDash { arg } => shown(here, arg.name),
_ => String::new(),
};
let mut out = String::new();
let mut with_usage = true;
match error {
Error::UnknownFlag { token } | Error::UnexpectedArg { token } => {
let word = String::from_utf8_lossy(token);
if matches!(error, Error::UnknownFlag { .. }) || (word.starts_with('-') && word != "-")
{
let typed = flag_named(&word);
let capture = value_capture(argv, token, here);
unexpected_flag(&mut out, style, typed, chain, capture);
} else if cmd.subcommands.is_empty() {
error_line(
&mut out,
style,
&[
(Plain, "unexpected argument '"),
(Invalid, &word),
(Plain, "' found"),
],
);
} else {
error_line(
&mut out,
style,
&[
(Plain, "unrecognized subcommand '"),
(Invalid, &word),
(Plain, "'"),
],
);
let mut names: Vec<&str> = Vec::new();
for sub in cmd.subcommands {
names.push(sub.name);
names.extend_from_slice(sub.aliases);
}
tip(&mut out, style, "subcommand", "", &nearest(&word, &names));
}
}
Error::SubcommandConflict { subcommand } => {
error_line(
&mut out,
style,
&[
(Plain, "the subcommand '"),
(Invalid, subcommand.name),
(
Plain,
"' cannot be used with arguments on its parent command",
),
],
);
}
Error::MissingRequired { .. } => {
error_line(
&mut out,
style,
&[(Plain, "the following required arguments were not provided:")],
);
listed_line(&mut out, style, &named);
}
Error::DuplicateFlag { .. } => {
error_line(
&mut out,
style,
&[
(Plain, "the argument '"),
(Invalid, &named),
(Plain, "' cannot be used multiple times"),
],
);
}
Error::MissingSubcommand => {
let help_style = crate::help::Style::coloured_if(style.coloured);
let presented_route: Vec<&Command<'_>> = chain.iter().map(|meta| meta.cmd).collect();
let help = crate::help::render_at_styled(spec, &presented_route, false, help_style);
if let Some(help) = help {
return help;
}
error_line(
&mut out,
style,
&[
(Plain, "'"),
(Invalid, &path()),
(Plain, "' requires a subcommand but one was not provided"),
],
);
}
Error::MissingFlagValue { flag } => {
with_usage = false;
let mut short = [0; 4];
let (dashes, spelled): (&str, &str) = if let Some(long) = flag.longs.first() {
("--", long)
} else if let Some(first) = flag.shorts.first() {
("-", (*first as char).encode_utf8(&mut short))
} else {
("", flag.name)
};
let value = here
.and_then(|meta| {
meta.flags
.iter()
.find(|m| core::ptr::eq(m.flag, *flag))
.and_then(|m| m.value_name)
})
.unwrap_or(flag.name);
let (before, joint, after) = if flag.require_equals {
("equal sign is needed when assigning values to '", "=<", "'")
} else {
("a value is required for '", " <", "' but none was supplied")
};
let mut wanted = String::new();
push_all(&mut wanted, &[dashes, spelled, joint, value, ">"]);
error_line(
&mut out,
style,
&[(Plain, before), (Invalid, &wanted), (Plain, after)],
);
}
Error::InvalidChoice { name, choices } => {
with_usage = false;
let typed = value_bound_to(canonical_root, argv, name, choices, view);
match typed.as_deref() {
Some(value) => error_line(
&mut out,
style,
&[
(Plain, "invalid value '"),
(Invalid, value),
(Plain, "' for '"),
(Literal, &named),
(Plain, "'"),
],
),
None => error_line(
&mut out,
style,
&[
(Plain, "invalid value for '"),
(Literal, &named),
(Plain, "'"),
],
),
}
push_all(&mut out, &[" [possible values: "]);
for (i, choice) in choices.iter().enumerate() {
style.put_all(
&mut out,
&[
(Plain, if i == 0 { "" } else { ", " }),
(Ink::Valid, choice),
],
);
}
push_all(&mut out, &["]\n"]);
if let Some(typed) = typed {
tip(&mut out, style, "value", "", &nearest(&typed, choices));
}
}
Error::InvalidValue(invalid) => {
with_usage = false;
error_line(
&mut out,
style,
&[
(Plain, "invalid value '"),
(Invalid, &invalid.value),
(Plain, "' for '"),
(Literal, &named),
(Plain, "': "),
(Plain, &invalid.reason),
],
);
}
Error::MissingGroup { group, members } => {
error_line(
&mut out,
style,
&[
(
Plain,
"one of the following required arguments was not provided (",
),
(Plain, group),
(Plain, "):"),
],
);
for member in *members {
listed_line(&mut out, style, &group_member_shown(here, member));
}
}
Error::ConflictingFlags { other, .. } => {
error_line(
&mut out,
style,
&[
(Plain, "the argument '"),
(Invalid, &named),
(Plain, "' cannot be used with '"),
(Invalid, &shown(here, other)),
(Plain, "'"),
],
);
}
Error::VarTooFew {
min: count, got, ..
}
| Error::VarTooMany {
max: count, got, ..
} => {
with_usage = false;
let bound = if matches!(error, Error::VarTooFew { .. }) {
" values required for '"
} else {
" values allowed for '"
};
error_line(
&mut out,
style,
&[
(Plain, &count.to_string()),
(Plain, bound),
(Literal, &named),
(Plain, "' but "),
(Plain, &got.to_string()),
(Plain, " were provided"),
],
);
}
Error::ArgRequiresDoubleDash { .. } => {
error_line(
&mut out,
style,
&[
(Plain, "'"),
(Literal, &named),
(Plain, "' can only be given after '"),
(Literal, "--"),
(Plain, "'"),
],
);
}
Error::TooDeep => {
with_usage = false;
error_line(
&mut out,
style,
&[(Plain, "this command line nests deeper than the parser goes")],
);
}
Error::Help { .. }
| Error::MissingArgsHelp { .. }
| Error::HelpAll { .. }
| Error::Version { .. } => {
return String::new();
}
}
if with_usage {
let usage = match (resolved, here) {
(Some((names, _)), Some(meta)) => crate::help::usage_line(names, meta),
_ => path(),
};
style.put_all(
&mut out,
&[
(Plain, "\n"),
(Ink::Heading, "Usage:"),
(Plain, " "),
(Literal, &usage),
(Plain, "\n"),
],
);
}
style.put_all(
&mut out,
&[
(Plain, "\nFor more information, try '"),
(Literal, "--help"),
(Plain, "'.\n"),
],
);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{ArgMeta, FlagMeta};
use crate::{Arg, Flag};
static FORCE: Flag = Flag {
key: 1,
name: "force",
longs: &["force"],
shorts: b"f",
negate: Some("no-force"),
..Flag::BOOL
};
static JOBS: Flag = Flag {
key: 2,
name: "jobs",
longs: &["jobs"],
..Flag::VALUE
};
static TOOL: Arg = Arg {
key: 3,
name: "TOOL",
..Arg::REQUIRED
};
static SHELLS: Arg = Arg {
key: 7,
name: "SHELLS",
..Arg::VAR
};
static USE: Command = Command {
name: "use",
flags: &[&FORCE, &JOBS],
args: &[&TOOL, &SHELLS],
..Command::EMPTY
};
static QUIET: Flag = Flag {
key: 4,
name: "quiet",
longs: &["quiet"],
global: true,
..Flag::BOOL
};
static LOCAL: Flag = Flag {
key: 5,
name: "local",
longs: &["local"],
global: true,
..Flag::BOOL
};
static USER: Command = Command {
name: "user",
flags: &[&LOCAL],
..Command::EMPTY
};
static SETUP: Flag = Flag {
key: 6,
name: "setup",
longs: &["setup"],
..Flag::BOOL
};
static CMDLINE: Arg = Arg {
key: 8,
name: "COMMAND",
double_dash: DoubleDash::Required,
..Arg::VAR
};
static EXEC: Command = Command {
name: "exec",
flags: &[&FORCE],
args: &[&CMDLINE],
..Command::EMPTY
};
static ROOT: Command = Command {
name: "ex",
flags: &[&QUIET, &SETUP],
subcommands: &[&USER, &USE, &EXEC],
..Command::EMPTY
};
static USE_META: CommandMeta = CommandMeta {
cmd: &USE,
about: Some("Use a tool"),
flags: &[
FlagMeta {
flag: &FORCE,
help: Some("Force it"),
..FlagMeta::EMPTY
},
FlagMeta {
flag: &JOBS,
help: Some("How many"),
value_name: Some("JOBS"),
..FlagMeta::EMPTY
},
],
args: &[
ArgMeta {
arg: &TOOL,
help: Some("Which tool"),
required: true,
..ArgMeta::EMPTY
},
ArgMeta {
arg: &SHELLS,
help: Some("Which shells"),
choices: &["bash", "zsh"],
required: false,
..ArgMeta::EMPTY
},
],
..CommandMeta::EMPTY
};
static USER_META: CommandMeta = CommandMeta {
cmd: &USER,
about: Some("Manage users"),
flags: &[FlagMeta {
flag: &LOCAL,
help: Some("Only this checkout"),
..FlagMeta::EMPTY
}],
..CommandMeta::EMPTY
};
static EXEC_META: CommandMeta = CommandMeta {
cmd: &EXEC,
about: Some("Run a command"),
flags: &[FlagMeta {
flag: &FORCE,
help: Some("Force it"),
..FlagMeta::EMPTY
}],
args: &[ArgMeta {
arg: &CMDLINE,
help: Some("What to run"),
required: true,
..ArgMeta::EMPTY
}],
..CommandMeta::EMPTY
};
static ROOT_META: CommandMeta = CommandMeta {
cmd: &ROOT,
flags: &[
FlagMeta {
flag: &QUIET,
help: Some("Say less"),
..FlagMeta::EMPTY
},
FlagMeta {
flag: &SETUP,
help: Some("Set things up"),
..FlagMeta::EMPTY
},
],
subcommands: &[&USER_META, &USE_META, &EXEC_META],
..CommandMeta::EMPTY
};
static SPEC: Spec = Spec {
name: "ex",
bin: Some("ex"),
root: &ROOT_META,
..Spec::EMPTY
};
fn rendered(words: &[&str], error: Error<'static, 'static>) -> String {
let owned: Vec<std::ffi::OsString> = words.iter().map(std::ffi::OsString::from).collect();
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|o| o.as_os_str()).collect();
render(&SPEC, &argv, &error, Style::PLAIN)
}
#[test]
fn a_structured_report_locates_the_exact_unknown_word() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("--fore"),
];
let argv = owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let error = Error::UnknownFlag {
token: argv[1].as_encoded_bytes(),
};
let report = report(&SPEC, &argv, &error);
assert_eq!(report.code, Code::UnknownFlag);
assert_eq!(report.code.as_str(), "unknown_flag");
assert_eq!(report.subject.as_deref(), Some("--fore"));
assert_eq!(
report.location,
Some(ArgvSpan {
index: 1,
start: 0,
end: 6,
})
);
assert!(report.rendered.starts_with("error: unexpected argument"));
}
#[test]
fn a_structured_report_locates_an_attached_invalid_value() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("--jobs=wat"),
std::ffi::OsString::from("tool"),
];
let argv = owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let error = Error::InvalidValue(Box::new(crate::InvalidValue {
name: "jobs",
value: "wat".to_string(),
reason: "invalid digit found in string".to_string(),
}));
let report = report(&SPEC, &argv, &error);
assert_eq!(report.code, Code::InvalidValue);
assert_eq!(
report.location,
Some(ArgvSpan {
index: 1,
start: 7,
end: 10,
})
);
}
#[test]
fn a_structured_report_finds_the_refused_value_in_a_variadic() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("tool"),
std::ffi::OsString::from("bash"),
std::ffi::OsString::from("fish"),
];
let argv = owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let error = Error::InvalidChoice {
name: "SHELLS",
choices: &["bash", "zsh"],
};
assert_eq!(
report(&SPEC, &argv, &error).location,
Some(ArgvSpan {
index: 3,
start: 0,
end: 4,
})
);
}
#[test]
fn a_missing_flag_value_points_at_the_flag() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("--jobs"),
];
let argv = owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let error = Error::MissingFlagValue { flag: &JOBS };
assert_eq!(
report(&SPEC, &argv, &error).location,
Some(ArgvSpan {
index: 1,
start: 0,
end: 6,
})
);
}
#[test]
fn a_repeated_missing_flag_points_at_the_occurrence_the_parser_refused() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("--jobs"),
std::ffi::OsString::from("4"),
std::ffi::OsString::from("--jobs"),
];
let argv = owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let error = Error::MissingFlagValue { flag: &JOBS };
assert_eq!(
report(&SPEC, &argv, &error).location,
Some(ArgvSpan {
index: 3,
start: 0,
end: 6,
})
);
}
#[test]
fn view_reports_map_invalid_values_back_to_the_original_argv() {
static VIEW: ViewMeta = ViewMeta {
id: "runner",
name: "runner",
bin: "runner",
root: "use",
all_globals: false,
globals: &[],
};
let invalid_owned = [
std::ffi::OsString::from("runner"),
std::ffi::OsString::from("--jobs=wat"),
];
let invalid_argv = invalid_owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let invalid = Error::InvalidValue(Box::new(crate::InvalidValue {
name: "jobs",
value: "wat".to_string(),
reason: "invalid digit found in string".to_string(),
}));
assert_eq!(
report_view(&SPEC, &invalid_argv, &invalid, &VIEW).location,
Some(ArgvSpan {
index: 1,
start: 7,
end: 10,
})
);
let choice_owned = [
std::ffi::OsString::from("runner"),
std::ffi::OsString::from("tool"),
std::ffi::OsString::from("fish"),
];
let choice_argv = choice_owned
.iter()
.map(|word| word.as_os_str())
.collect::<Vec<_>>();
let choice = Error::InvalidChoice {
name: "SHELLS",
choices: &["bash", "zsh"],
};
assert_eq!(
report_view(&SPEC, &choice_argv, &choice, &VIEW).location,
Some(ArgvSpan {
index: 2,
start: 0,
end: 4,
})
);
}
#[test]
fn the_usage_line_is_the_one_the_help_prints() {
let message = rendered(&["use"], Error::UnknownFlag { token: b"--fore" });
let line = message
.lines()
.find_map(|l| l.strip_prefix("Usage: "))
.expect("a usage line");
assert_eq!(line, crate::help::usage_line(&["ex", "use"], &USE_META));
}
#[test]
fn a_missing_group_lists_value_taking_members_completely() {
static FILE: Flag = Flag {
name: "file",
longs: &["file"],
takes_value: true,
..Flag::BOOL
};
static ROOT: Command = Command {
name: "grouped",
flags: &[&FILE],
..Command::EMPTY
};
static META: CommandMeta = CommandMeta {
cmd: &ROOT,
flags: &[FlagMeta {
flag: &FILE,
value_name: Some("PATH"),
..FlagMeta::EMPTY
}],
..CommandMeta::EMPTY
};
static SPEC: Spec = Spec {
name: "grouped",
bin: Some("grouped"),
root: &META,
..Spec::EMPTY
};
let message = render(
&SPEC,
&[],
&Error::MissingGroup {
group: "input",
members: &["--file"],
},
Style::PLAIN,
);
assert!(message.contains(" --file <PATH>"), "{message}");
}
#[test]
fn a_missing_subcommand_prints_the_choices() {
let message = rendered(&[], Error::MissingSubcommand);
assert!(message.contains("Commands:"), "{message}");
assert!(message.contains("use"), "{message}");
assert!(message.contains("user"), "{message}");
assert!(!message.contains("requires a subcommand"), "{message}");
let coloured = render(&SPEC, &[], &Error::MissingSubcommand, Style::COLOURED);
assert!(
coloured.contains("\u{1b}[1;33mCommands:\u{1b}[0m"),
"{coloured}"
);
}
#[test]
fn a_view_missing_subcommand_prints_the_promoted_choices() {
static CHILD: Command = Command {
name: "child",
..Command::EMPTY
};
static RUN: Command = Command {
name: "run",
subcommands: &[&CHILD],
..Command::EMPTY
};
static HOST: Command = Command {
name: "host",
subcommands: &[&RUN],
..Command::EMPTY
};
static CHILD_META: CommandMeta = CommandMeta {
cmd: &CHILD,
about: Some("Do the child thing"),
..CommandMeta::EMPTY
};
static RUN_META: CommandMeta = CommandMeta {
cmd: &RUN,
subcommands: &[&CHILD_META],
..CommandMeta::EMPTY
};
static HOST_META: CommandMeta = CommandMeta {
cmd: &HOST,
subcommands: &[&RUN_META],
..CommandMeta::EMPTY
};
static VIEW: ViewMeta = ViewMeta {
id: "runner",
name: "runner",
bin: "runner",
root: "run",
all_globals: false,
globals: &[],
};
static VIEW_SPEC: Spec = Spec {
name: "host",
bin: Some("host"),
root: &HOST_META,
views: &[VIEW],
..Spec::EMPTY
};
let message = render_view(
&VIEW_SPEC,
&[std::ffi::OsStr::new("runner")],
&Error::MissingSubcommand,
Style::PLAIN,
&VIEW,
);
assert!(message.contains("Usage: runner"), "{message}");
assert!(message.contains("Commands:"), "{message}");
assert!(message.contains("child"), "{message}");
assert!(!message.contains("requires a subcommand"), "{message}");
}
#[test]
fn a_missing_value_names_what_it_wanted() {
assert_eq!(
rendered(&["use"], Error::MissingFlagValue { flag: &JOBS }),
"error: a value is required for '--jobs <JOBS>' but none was supplied\n\
\n\
For more information, try '--help'.\n"
);
}
#[test]
fn a_word_where_a_subcommand_was_expected_says_so() {
let at_root = rendered(&[], Error::UnexpectedArg { token: b"nonesuch" });
assert!(
at_root.starts_with("error: unrecognized subcommand 'nonesuch'"),
"{at_root}"
);
let in_use = rendered(&["use"], Error::UnexpectedArg { token: b"extra" });
assert!(
in_use.starts_with("error: unexpected argument 'extra' found"),
"{in_use}"
);
}
#[test]
fn a_required_argument_is_listed_the_way_clap_lists_it() {
assert_eq!(
rendered(&["use"], Error::MissingRequired { name: "<TOOL>" }),
"error: the following required arguments were not provided:\n \
<TOOL>\n\
\n\
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…\n\
\n\
For more information, try '--help'.\n"
);
}
#[test]
fn colour_is_the_same_codes_clap_uses() {
let owned = [std::ffi::OsString::from("use")];
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|o| o.as_os_str()).collect();
let message = render(
&SPEC,
&argv,
&Error::UnknownFlag { token: b"--fore" },
Style::COLOURED,
);
assert!(
message.starts_with("\u{1b}[1m\u{1b}[31merror:\u{1b}[0m"),
"{message:?}"
);
assert!(message.contains("\u{1b}[33m--fore\u{1b}[0m"), "{message:?}");
assert!(
message.contains("\u{1b}[1m\u{1b}[4mUsage:\u{1b}[0m"),
"{message:?}"
);
assert!(!rendered(&["use"], Error::UnknownFlag { token: b"--fore" }).contains('\u{1b}'));
}
#[test]
fn a_help_request_renders_nothing() {
assert_eq!(
rendered(
&["use"],
Error::Help {
cmd: &USE,
long: true
}
),
""
);
}
#[test]
fn a_name_is_spelled_the_way_the_help_spells_it() {
let message = rendered(&["use"], Error::MissingRequired { name: "TOOL" });
assert!(message.contains(" <TOOL>"), "{message}");
let message = rendered(
&["use"],
Error::VarTooFew {
name: "SHELLS",
min: 2,
got: 1,
},
);
assert!(message.contains("'[SHELLS]…'"), "{message}");
let message = rendered(&["use"], Error::MissingRequired { name: "jobs" });
assert!(message.contains(" --jobs"), "{message}");
let message = rendered(&["use"], Error::DuplicateFlag { name: "jobs" });
assert!(
message.contains("the argument '--jobs' cannot be used multiple times"),
"{message}"
);
let message = rendered(
&["use"],
Error::ConflictingFlags {
name: "force",
other: "jobs",
},
);
assert!(
message.contains("the argument '--force' cannot be used with '--jobs'"),
"{message}"
);
let message = rendered(&["use"], Error::ArgRequiresDoubleDash { arg: &SHELLS });
assert!(
message.contains("'[SHELLS]…' can only be given"),
"{message}"
);
}
#[test]
fn the_value_named_is_the_one_that_was_refused() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("node"),
std::ffi::OsString::from("fsh"),
std::ffi::OsString::from("zsh"),
];
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|o| o.as_os_str()).collect();
let message = render(
&SPEC,
&argv,
&Error::InvalidChoice {
name: "SHELLS",
choices: &["bash", "zsh"],
},
Style::PLAIN,
);
assert!(
message.starts_with("error: invalid value 'fsh' for '[SHELLS]…'"),
"named a value that was fine: {message}"
);
}
#[test]
fn a_view_recovers_the_choice_value_from_the_canonical_parse() {
static VIEW: ViewMeta = ViewMeta {
id: "runner",
name: "runner",
bin: "runner",
root: "use",
all_globals: false,
globals: &[],
};
static VIEW_SPEC: Spec = Spec {
views: &[VIEW],
..SPEC
};
let argv = [std::ffi::OsStr::new("runner"), std::ffi::OsStr::new("nod")];
let message = render_view(
&VIEW_SPEC,
&argv,
&Error::InvalidChoice {
name: "TOOL",
choices: &["node", "python"],
},
Style::PLAIN,
&VIEW,
);
assert!(
message.contains("invalid value 'nod' for '<TOOL>'"),
"{message}"
);
assert!(
message.contains("tip: a similar value exists: 'node'"),
"{message}"
);
}
#[test]
fn a_near_miss_is_suggested_the_way_clap_suggests_one() {
let message = rendered(&["use"], Error::UnknownFlag { token: b"--fore" });
assert_eq!(
message,
"error: unexpected argument '--fore' found\n\
\n\
\x20 tip: a similar argument exists: '--force'\n\
\n\
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…\n\
\n\
For more information, try '--help'.\n"
);
}
#[test]
fn a_value_attached_to_a_flag_is_not_part_of_its_name() {
assert_eq!(
rendered(&["use"], Error::UnknownFlag { token: b"--fore=1" }),
"error: unexpected argument '--fore' found\n\
\n\
\x20 tip: a similar argument exists: '--force'\n\
\n\
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…\n\
\n\
For more information, try '--help'.\n"
);
let message = rendered(&["use"], Error::UnknownFlag { token: b"-j=4" });
assert!(
message.starts_with("error: unexpected argument '-j=4' found"),
"{message}"
);
}
#[test]
fn a_negation_is_suggested_like_any_other_spelling() {
let message = rendered(
&["use"],
Error::UnknownFlag {
token: b"--no-forc",
},
);
assert!(
message.contains("tip: a similar argument exists: '--no-force'"),
"{message}"
);
let message = rendered(&["use"], Error::UnknownFlag { token: b"--fore" });
assert!(
message.contains("tip: a similar argument exists: '--force'"),
"{message}"
);
}
#[test]
fn nothing_is_suggested_when_nothing_is_close() {
assert_eq!(
rendered(&["use"], Error::UnknownFlag { token: b"--zzz" }),
"error: unexpected argument '--zzz' found\n\
\n\
\x20 tip: to pass '--zzz' as a value, use '-- --zzz'\n\
\n\
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…\n\
\n\
For more information, try '--help'.\n"
);
}
#[test]
fn a_command_that_collects_a_command_line_offers_the_separator_too() {
assert_eq!(
rendered(&["exec"], Error::UnknownFlag { token: b"--fore" }),
"error: unexpected argument '--fore' found\n\
\n\
\x20 tip: a similar argument exists: '--force'\n\
\x20 tip: to pass '--fore' as a value, use '-- --fore'\n\
\n\
Usage: ex exec [-f --force] <-- COMMAND>…\n\
\n\
For more information, try '--help'.\n"
);
let ordinary = rendered(&["use"], Error::UnknownFlag { token: b"--fore" });
assert!(
ordinary.contains("tip: a similar argument exists: '--force'"),
"{ordinary}"
);
assert!(!ordinary.contains("to pass"), "{ordinary}");
}
#[test]
fn the_separator_is_not_suggested_once_it_has_been_given() {
let owned = [
std::ffi::OsString::from("exec"),
std::ffi::OsString::from("--"),
std::ffi::OsString::from("--zzz"),
];
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|word| word.as_os_str()).collect();
let message = render(
&SPEC,
&argv,
&Error::UnknownFlag {
token: argv[2].as_encoded_bytes(),
},
Style::PLAIN,
);
assert!(!message.contains("to pass"), "{message}");
}
#[test]
fn a_separator_later_on_the_line_does_not_count_as_already_given() {
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("--zzz"),
std::ffi::OsString::from("--"),
std::ffi::OsString::from("tail"),
];
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|word| word.as_os_str()).collect();
let message = render(
&SPEC,
&argv,
&Error::UnknownFlag { token: b"--zzz" },
Style::PLAIN,
);
assert!(
message.contains("to pass '--zzz' as a value, use '-- --zzz'"),
"{message}"
);
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("--"),
std::ffi::OsString::from("--zzz"),
];
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|word| word.as_os_str()).collect();
let message = render(
&SPEC,
&argv,
&Error::UnknownFlag { token: b"--zzz" },
Style::PLAIN,
);
assert!(!message.contains("to pass"), "{message}");
}
#[test]
fn a_repeated_word_is_answered_for_every_occurrence_of_it() {
let render_literal = |words: &[&str]| {
let owned: Vec<std::ffi::OsString> =
words.iter().map(std::ffi::OsString::from).collect();
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|word| word.as_os_str()).collect();
render(
&SPEC,
&argv,
&Error::UnknownFlag { token: b"--zzz" },
Style::PLAIN,
)
};
let split = render_literal(&["use", "--zzz", "--", "--zzz"]);
assert!(split.contains("to pass '--zzz' as a value"), "{split}");
let after = render_literal(&["use", "--", "--zzz", "--zzz"]);
assert!(!after.contains("to pass"), "{after}");
}
#[test]
fn the_separator_is_not_suggested_where_nothing_takes_a_value() {
let message = rendered(&["user"], Error::UnknownFlag { token: b"--zzz" });
assert!(!message.contains("to pass"), "{message}");
}
#[test]
fn the_scores_are_the_ones_clap_would_compute() {
assert!(
(jaro("fore", "force") - 0.933).abs() < 0.001,
"{}",
jaro("fore", "force")
);
assert_eq!(jaro("same", "same"), 1.0);
assert_eq!(jaro("", ""), 1.0);
assert_eq!(jaro("abc", ""), 0.0);
assert_eq!(jaro("abc", "xyz"), 0.0);
assert_eq!(jaro("forc", "force"), jaro("orce", "force"));
}
#[test]
fn a_subcommand_and_a_value_get_the_same_treatment() {
let message = rendered(&[], Error::UnexpectedArg { token: b"usse" });
assert!(
message.contains("tip: some similar subcommands exist: 'user', 'use'"),
"{message}"
);
let owned = [
std::ffi::OsString::from("use"),
std::ffi::OsString::from("nod"),
];
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|o| o.as_os_str()).collect();
let message = render(
&SPEC,
&argv,
&Error::InvalidChoice {
name: "TOOL",
choices: &["node", "python"],
},
Style::PLAIN,
);
assert!(
message.contains("invalid value 'nod' for '<TOOL>'"),
"{message}"
);
assert!(
message.contains("tip: a similar value exists: 'node'"),
"{message}"
);
}
#[test]
fn a_global_flag_is_suggested_inside_a_subcommand() {
let message = rendered(&["use"], Error::UnknownFlag { token: b"--quie" });
assert!(
message.contains("tip: a similar argument exists: '--quiet'"),
"{message}"
);
}
#[test]
fn a_dash_prefixed_word_is_a_flag_even_where_subcommands_exist() {
let message = rendered(&[], Error::UnexpectedArg { token: b"--quie" });
assert!(
message.starts_with("error: unexpected argument '--quie' found"),
"{message}"
);
assert!(
message.contains("tip: a similar argument exists: '--quiet'"),
"{message}"
);
assert!(!message.contains("subcommand"), "{message}");
let message = rendered(&[], Error::UnexpectedArg { token: b"usse" });
assert!(
message.starts_with("error: unrecognized subcommand 'usse'"),
"{message}"
);
let message = rendered(&[], Error::UnexpectedArg { token: b"-" });
assert!(
message.starts_with("error: unrecognized subcommand '-'"),
"{message}"
);
}
#[test]
fn a_siblings_global_is_not_offered_here() {
let message = rendered(&["use"], Error::UnknownFlag { token: b"--locl" });
assert!(!message.contains("a similar argument exists"), "{message}");
let message = rendered(&["user"], Error::UnknownFlag { token: b"--locl" });
assert!(
message.contains("tip: a similar argument exists: '--local'"),
"{message}"
);
let message = rendered(&["use"], Error::UnknownFlag { token: b"--quie" });
assert!(
message.contains("tip: a similar argument exists: '--quiet'"),
"{message}"
);
let message = rendered(&["use"], Error::UnknownFlag { token: b"--setu" });
assert!(!message.contains("a similar argument exists"), "{message}");
let message = rendered(&[], Error::UnknownFlag { token: b"--setu" });
assert!(
message.contains("tip: a similar argument exists: '--setup'"),
"{message}"
);
}
fn every_shape(style: Style) -> String {
static EQUALS: Flag = Flag {
key: 9,
name: "mode",
longs: &["mode"],
require_equals: true,
..Flag::VALUE
};
let cases: Vec<(&[&str], Error<'static, 'static>)> = vec![
(&["use"], Error::UnknownFlag { token: b"--fore" }),
(&["use"], Error::UnknownFlag { token: b"--zzz" }),
(&["exec"], Error::UnknownFlag { token: b"--fore" }),
(&["user"], Error::UnknownFlag { token: b"--zzz" }),
(&[], Error::UnexpectedArg { token: b"usse" }),
(&[], Error::UnexpectedArg { token: b"usr" }),
(&[], Error::UnexpectedArg { token: b"zzz" }),
(&[], Error::UnexpectedArg { token: b"--quie" }),
(&["use"], Error::UnexpectedArg { token: b"extra" }),
(&["use"], Error::SubcommandConflict { subcommand: &USE }),
(&["use"], Error::MissingRequired { name: "TOOL" }),
(&["use"], Error::DuplicateFlag { name: "jobs" }),
(&["use"], Error::MissingFlagValue { flag: &JOBS }),
(&["use"], Error::MissingFlagValue { flag: &FORCE }),
(&["use"], Error::MissingFlagValue { flag: &EQUALS }),
(
&["use", "tool", "bash", "zssh"],
Error::InvalidChoice {
name: "SHELLS",
choices: &["bash", "zsh"],
},
),
(
&["use"],
Error::InvalidChoice {
name: "SHELLS",
choices: &["bash", "zsh"],
},
),
(
&["use", "--jobs", "wat"],
Error::InvalidValue(Box::new(crate::InvalidValue {
name: "jobs",
value: "wat".to_string(),
reason: "invalid digit found in string".to_string(),
})),
),
(
&["use"],
Error::MissingGroup {
group: "input",
members: &["--jobs", "-f", "--no-force", "--elsewhere"],
},
),
(
&["use"],
Error::ConflictingFlags {
name: "force",
other: "jobs",
},
),
(
&["use"],
Error::VarTooFew {
name: "SHELLS",
min: 2,
got: 1,
},
),
(
&["use"],
Error::VarTooMany {
name: "SHELLS",
max: 1,
got: 3,
},
),
(&["exec"], Error::ArgRequiresDoubleDash { arg: &CMDLINE }),
(&["use"], Error::TooDeep),
];
let mut all = String::new();
for (words, error) in cases {
let owned: Vec<std::ffi::OsString> =
words.iter().map(std::ffi::OsString::from).collect();
let argv: Vec<&std::ffi::OsStr> = owned.iter().map(|o| o.as_os_str()).collect();
all.push_str(&render(&SPEC, &argv, &error, style));
all.push_str("----\n");
}
static VIEW: ViewMeta = ViewMeta {
id: "runner",
name: "runner",
bin: "runner",
root: "use",
all_globals: true,
globals: &[],
};
static VIEW_SPEC: Spec = Spec {
views: &[VIEW],
..SPEC
};
let view_cases: Vec<(&[&str], Error<'static, 'static>)> = vec![
(
&["runner", "--fore"],
Error::UnknownFlag { token: b"--fore" },
),
(
&["runner", "--quie"],
Error::UnknownFlag { token: b"--quie" },
),
(&["runner"], Error::MissingRequired { name: "TOOL" }),
(
&["runner", "tool", "zssh"],
Error::InvalidChoice {
name: "SHELLS",
choices: &["bash", "zsh"],
},
),
];
for (words, error) in view_cases {
let argv: Vec<&std::ffi::OsStr> = words.iter().map(std::ffi::OsStr::new).collect();
all.push_str(&render_view(&VIEW_SPEC, &argv, &error, style, &VIEW));
all.push_str("----\n");
}
all.replace('\u{1b}', "^[")
}
#[test]
fn a_warning_is_coloured_like_a_failure() {
use crate::warn::Warning;
let warnings = [
Warning::flag("--old", None, None, Some("2.0.0")),
Warning::flag("--old", Some("use --new"), None, None),
Warning::env("OLD_TOKEN", Some("APP_TOKEN")),
];
assert_eq!(
render_warnings(&warnings, Style::PLAIN),
"warning: --old is deprecated, removed at 2.0.0\n\
warning: --old is deprecated: use --new\n\
warning: OLD_TOKEN is deprecated: use APP_TOKEN\n",
);
assert_eq!(
render_warnings(&warnings, Style::COLOURED).replace('\u{1b}', "^["),
"^[[1m^[[33mwarning:^[[0m ^[[33m--old^[[0m is deprecated, removed at ^[[1m2.0.0^[[0m\n\
^[[1m^[[33mwarning:^[[0m ^[[33m--old^[[0m is deprecated: use --new\n\
^[[1m^[[33mwarning:^[[0m ^[[33mOLD_TOKEN^[[0m is deprecated: use ^[[32mAPP_TOKEN^[[0m\n",
);
}
#[test]
fn every_message_is_unchanged_plain() {
let got = every_shape(Style::PLAIN);
assert_eq!(got, EVERY_SHAPE_PLAIN, "\n{got}");
}
#[test]
fn every_message_is_unchanged_coloured() {
let got = every_shape(Style::COLOURED);
assert_eq!(got, EVERY_SHAPE_COLOURED, "\n{got}");
}
const EVERY_SHAPE_PLAIN: &str = r#"error: unexpected argument '--fore' found
tip: a similar argument exists: '--force'
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: unexpected argument '--zzz' found
tip: to pass '--zzz' as a value, use '-- --zzz'
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: unexpected argument '--fore' found
tip: a similar argument exists: '--force'
tip: to pass '--fore' as a value, use '-- --fore'
Usage: ex exec [-f --force] <-- COMMAND>…
For more information, try '--help'.
----
error: unexpected argument '--zzz' found
Usage: ex user [--local]
For more information, try '--help'.
----
error: unrecognized subcommand 'usse'
tip: some similar subcommands exist: 'user', 'use'
Usage: ex [--quiet] [--setup] [SUBCOMMAND]
For more information, try '--help'.
----
error: unrecognized subcommand 'usr'
tip: some similar subcommands exist: 'use', 'user'
Usage: ex [--quiet] [--setup] [SUBCOMMAND]
For more information, try '--help'.
----
error: unrecognized subcommand 'zzz'
Usage: ex [--quiet] [--setup] [SUBCOMMAND]
For more information, try '--help'.
----
error: unexpected argument '--quie' found
tip: a similar argument exists: '--quiet'
Usage: ex [--quiet] [--setup] [SUBCOMMAND]
For more information, try '--help'.
----
error: unexpected argument 'extra' found
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: the subcommand 'use' cannot be used with arguments on its parent command
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: the following required arguments were not provided:
<TOOL>
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: the argument '--jobs' cannot be used multiple times
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: a value is required for '--jobs <JOBS>' but none was supplied
For more information, try '--help'.
----
error: a value is required for '--force <force>' but none was supplied
For more information, try '--help'.
----
error: equal sign is needed when assigning values to '--mode=<mode>'
For more information, try '--help'.
----
error: invalid value 'zssh' for '[SHELLS]…'
[possible values: bash, zsh]
tip: a similar value exists: 'zsh'
For more information, try '--help'.
----
error: invalid value for '[SHELLS]…'
[possible values: bash, zsh]
For more information, try '--help'.
----
error: invalid value 'wat' for '--jobs': invalid digit found in string
For more information, try '--help'.
----
error: one of the following required arguments was not provided (input):
--jobs <JOBS>
--force
--force
--elsewhere
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: the argument '--force' cannot be used with '--jobs'
Usage: ex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: 2 values required for '[SHELLS]…' but 1 were provided
For more information, try '--help'.
----
error: 1 values allowed for '[SHELLS]…' but 3 were provided
For more information, try '--help'.
----
error: '<-- COMMAND>…' can only be given after '--'
Usage: ex exec [-f --force] <-- COMMAND>…
For more information, try '--help'.
----
error: this command line nests deeper than the parser goes
For more information, try '--help'.
----
error: unexpected argument '--fore' found
tip: a similar argument exists: '--force'
Usage: runner [FLAGS] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: unexpected argument '--quie' found
tip: a similar argument exists: '--quiet'
Usage: runner [FLAGS] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: the following required arguments were not provided:
<TOOL>
Usage: runner [FLAGS] <TOOL> [SHELLS]…
For more information, try '--help'.
----
error: invalid value 'zssh' for '[SHELLS]…'
[possible values: bash, zsh]
tip: a similar value exists: 'zsh'
For more information, try '--help'.
----
"#;
const EVERY_SHAPE_COLOURED: &str = r#"^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--fore^[[0m' found
^[[32mtip:^[[0m a similar argument exists: '^[[32m--force^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--zzz^[[0m' found
^[[32mtip:^[[0m to pass '^[[33m--zzz^[[0m' as a value, use '^[[32m-- --zzz^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--fore^[[0m' found
^[[32mtip:^[[0m a similar argument exists: '^[[32m--force^[[0m'
^[[32mtip:^[[0m to pass '^[[33m--fore^[[0m' as a value, use '^[[32m-- --fore^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex exec [-f --force] <-- COMMAND>…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--zzz^[[0m' found
^[[1m^[[4mUsage:^[[0m ^[[1mex user [--local]^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unrecognized subcommand '^[[33musse^[[0m'
^[[32mtip:^[[0m some similar subcommands exist: '^[[32muser^[[0m', '^[[32muse^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex [--quiet] [--setup] [SUBCOMMAND]^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unrecognized subcommand '^[[33musr^[[0m'
^[[32mtip:^[[0m some similar subcommands exist: '^[[32muse^[[0m', '^[[32muser^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex [--quiet] [--setup] [SUBCOMMAND]^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unrecognized subcommand '^[[33mzzz^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex [--quiet] [--setup] [SUBCOMMAND]^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--quie^[[0m' found
^[[32mtip:^[[0m a similar argument exists: '^[[32m--quiet^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex [--quiet] [--setup] [SUBCOMMAND]^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33mextra^[[0m' found
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m the subcommand '^[[33muse^[[0m' cannot be used with arguments on its parent command
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m the following required arguments were not provided:
^[[32m<TOOL>^[[0m
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m the argument '^[[33m--jobs^[[0m' cannot be used multiple times
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m a value is required for '^[[33m--jobs <JOBS>^[[0m' but none was supplied
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m a value is required for '^[[33m--force <force>^[[0m' but none was supplied
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m equal sign is needed when assigning values to '^[[33m--mode=<mode>^[[0m'
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m invalid value '^[[33mzssh^[[0m' for '^[[1m[SHELLS]…^[[0m'
[possible values: ^[[32mbash^[[0m, ^[[32mzsh^[[0m]
^[[32mtip:^[[0m a similar value exists: '^[[32mzsh^[[0m'
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m invalid value for '^[[1m[SHELLS]…^[[0m'
[possible values: ^[[32mbash^[[0m, ^[[32mzsh^[[0m]
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m invalid value '^[[33mwat^[[0m' for '^[[1m--jobs^[[0m': invalid digit found in string
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m one of the following required arguments was not provided (input):
^[[32m--jobs <JOBS>^[[0m
^[[32m--force^[[0m
^[[32m--force^[[0m
^[[32m--elsewhere^[[0m
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m the argument '^[[33m--force^[[0m' cannot be used with '^[[33m--jobs^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex use [-f --force] [--jobs <JOBS>] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m 2 values required for '^[[1m[SHELLS]…^[[0m' but 1 were provided
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m 1 values allowed for '^[[1m[SHELLS]…^[[0m' but 3 were provided
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m '^[[1m<-- COMMAND>…^[[0m' can only be given after '^[[1m--^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mex exec [-f --force] <-- COMMAND>…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m this command line nests deeper than the parser goes
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--fore^[[0m' found
^[[32mtip:^[[0m a similar argument exists: '^[[32m--force^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mrunner [FLAGS] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m unexpected argument '^[[33m--quie^[[0m' found
^[[32mtip:^[[0m a similar argument exists: '^[[32m--quiet^[[0m'
^[[1m^[[4mUsage:^[[0m ^[[1mrunner [FLAGS] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m the following required arguments were not provided:
^[[32m<TOOL>^[[0m
^[[1m^[[4mUsage:^[[0m ^[[1mrunner [FLAGS] <TOOL> [SHELLS]…^[[0m
For more information, try '^[[1m--help^[[0m'.
----
^[[1m^[[31merror:^[[0m invalid value '^[[33mzssh^[[0m' for '^[[1m[SHELLS]…^[[0m'
[possible values: ^[[32mbash^[[0m, ^[[32mzsh^[[0m]
^[[32mtip:^[[0m a similar value exists: '^[[32mzsh^[[0m'
For more information, try '^[[1m--help^[[0m'.
----
"#;
}