#![forbid(unsafe_code)]
#[doc(hidden)]
#[allow(clippy::disallowed_methods)]
pub fn __usage_process_exit(status: i32) -> ! {
std::process::exit(status)
}
use std::ffi::{OsStr, OsString};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValueHint {
Unknown,
Other,
FilePath,
AnyPath,
DirPath,
ExecutablePath,
CommandName,
CommandString,
CommandWithArguments,
Username,
Hostname,
Url,
EmailAddress,
}
#[cfg(feature = "complete")]
pub mod complete;
#[cfg(feature = "diagnostics")]
pub mod diagnostic;
#[cfg(feature = "spec")]
pub mod embedded;
#[cfg(feature = "complete")]
pub mod install;
#[cfg(feature = "complete")]
pub mod script;
#[cfg(feature = "complete")]
#[macro_export]
macro_rules! __usage_needs_complete_feature {
() => {};
}
#[cfg(not(feature = "complete"))]
#[macro_export]
macro_rules! __usage_needs_complete_feature {
() => {
::core::compile_error!(
"`#[usage(completion)]` needs usage-argv's `complete` feature. Add it where \
usage-argv is depended on: usage-argv = { version = \"…\", features = \
[\"spec\", \"complete\"] }"
);
};
}
#[cfg(feature = "spec")]
pub mod help;
pub mod run;
#[cfg(feature = "spec")]
pub mod spec;
#[cfg(feature = "spec")]
pub mod warn;
pub use run::{Run, RunAsync, RunAsyncWith, RunWith};
pub const MAX_DEPTH: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Command<'a> {
pub name: &'a str,
pub aliases: &'a [&'a str],
pub flags: &'a [&'a Flag<'a>],
pub args: &'a [&'a Arg<'a>],
pub subcommands: &'a [&'a Command<'a>],
pub default_subcommand: ::core::option::Option<&'a Command<'a>>,
pub external_subcommand: bool,
pub arg_required_else_help: bool,
pub subcommand_negates_reqs: bool,
pub args_conflicts_with_subcommands: bool,
pub subcommand_precedence_over_arg: bool,
pub allow_missing_positional: bool,
pub dont_delimit_trailing_values: bool,
pub unknown_flags: ::core::option::Option<UnknownFlags>,
pub version: bool,
pub disable_help_flag: bool,
pub disable_help_subcommand: bool,
pub disable_version_flag: bool,
pub key: u64,
}
impl Command<'_> {
pub const EMPTY: Command<'static> = Command {
name: "",
aliases: &[],
flags: &[],
args: &[],
subcommands: &[],
default_subcommand: ::core::option::Option::None,
external_subcommand: false,
arg_required_else_help: false,
subcommand_negates_reqs: false,
args_conflicts_with_subcommands: false,
subcommand_precedence_over_arg: false,
allow_missing_positional: false,
dont_delimit_trailing_values: false,
unknown_flags: ::core::option::Option::None,
version: false,
disable_help_flag: false,
disable_help_subcommand: false,
disable_version_flag: false,
key: 0,
};
}
pub fn multicall_basename(argv0: &str) -> &str {
let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
match name.get(name.len().saturating_sub(4)..) {
Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
_ => name,
}
}
pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
let base = multicall_basename(argv0);
if !name.is_empty() && base == multicall_basename(name) {
return None;
}
if let Some(bin) = bin {
if !bin.is_empty() && base == multicall_basename(bin) {
return None;
}
}
Some(base)
}
#[derive(Clone, Copy)]
pub struct BindingType(pub fn() -> &'static str);
impl BindingType {
pub fn name(self) -> &'static str {
(self.0)()
}
}
impl ::core::fmt::Debug for BindingType {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_tuple("BindingType").field(&self.name()).finish()
}
}
impl PartialEq for BindingType {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
}
}
impl Eq for BindingType {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Flag<'a> {
pub key: u64,
pub binding_key: u64,
pub binding_type: Option<BindingType>,
pub name: &'a str,
pub longs: &'a [&'a str],
pub shorts: &'a [u8],
pub negate: Option<&'a str>,
pub takes_value: bool,
pub variadic: bool,
pub var_max: ::core::option::Option<u32>,
pub delimiter: ::core::option::Option<u8>,
pub allow_hyphen_values: bool,
pub allow_negative_numbers: bool,
pub value_terminator: ::core::option::Option<&'a [u8]>,
pub require_equals: bool,
pub value_optional: bool,
pub bool_value: bool,
pub default_missing: ::core::option::Option<&'a [u8]>,
pub global: bool,
pub action: ArgAction,
}
impl Flag<'_> {
pub const BOOL: Flag<'static> = Flag {
key: 0,
binding_key: 0,
binding_type: None,
name: "",
longs: &[],
shorts: &[],
negate: None,
takes_value: false,
variadic: false,
var_max: ::core::option::Option::None,
delimiter: ::core::option::Option::None,
allow_hyphen_values: false,
allow_negative_numbers: false,
value_terminator: ::core::option::Option::None,
require_equals: false,
value_optional: false,
bool_value: false,
default_missing: ::core::option::Option::None,
global: false,
action: ArgAction::Set,
};
pub const VALUE: Flag<'static> = Flag {
takes_value: true,
..Flag::BOOL
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ArgAction {
#[default]
Set,
Help,
HelpShort,
HelpLong,
HelpAll,
Version,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Arg<'a> {
pub key: u64,
pub required: bool,
pub var: bool,
pub var_max: ::core::option::Option<u32>,
pub delimiter: ::core::option::Option<u8>,
pub allow_negative_numbers: bool,
pub value_terminator: ::core::option::Option<&'a [u8]>,
pub double_dash: DoubleDash,
pub name: &'a str,
}
impl Arg<'_> {
pub const REQUIRED: Arg<'static> = Arg {
key: 0,
required: true,
var: false,
var_max: ::core::option::Option::None,
delimiter: ::core::option::Option::None,
allow_negative_numbers: false,
value_terminator: ::core::option::Option::None,
double_dash: DoubleDash::Optional,
name: "",
};
pub const VAR: Arg<'static> = Arg {
var: true,
..Arg::REQUIRED
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UnknownFlags {
#[default]
Value,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DoubleDash {
#[default]
Optional,
Required,
Preserve,
Automatic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event<'t, 'a, 'v> {
Command(&'t Command<'t>),
Flag {
flag: &'t Flag<'t>,
value: Option<&'v [u8]>,
negated: bool,
},
Arg {
arg: &'t Arg<'t>,
value: &'v [u8],
delimit: bool,
},
External { values: &'a [&'v OsStr] },
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error<'t, 'v> {
UnknownFlag { token: &'v [u8] },
MissingFlagValue { flag: &'t Flag<'t> },
UnexpectedArg { token: &'v [u8] },
ArgRequiresDoubleDash { arg: &'t Arg<'t> },
SubcommandConflict { subcommand: &'t Command<'t> },
TooDeep,
MissingRequired {
name: &'t str,
},
DuplicateFlag {
name: &'t str,
},
InvalidChoice {
name: &'t str,
choices: &'t [&'t str],
},
VarTooFew {
name: &'t str,
min: usize,
got: usize,
},
VarTooMany {
name: &'t str,
max: usize,
got: usize,
},
ConflictingFlags {
name: &'t str,
other: &'t str,
},
InvalidValue(::std::boxed::Box<InvalidValue<'t>>),
MissingGroup {
group: &'t str,
members: &'t [&'t str],
},
MissingSubcommand,
Help { cmd: &'t Command<'t>, long: bool },
MissingArgsHelp { cmd: &'t Command<'t> },
HelpAll { cmd: &'t Command<'t> },
Version { long: bool },
}
pub const fn key_base(module: &str, declaration: u32) -> u64 {
let mut hash: u32 = declaration;
let bytes = module.as_bytes();
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u32;
hash = hash.wrapping_mul(0x0100_0193);
i += 1;
}
(hash as u64) << 32
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidValue<'t> {
pub name: &'t str,
pub value: ::std::string::String,
pub reason: ::std::string::String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
name: &'static str,
value: String,
reason: String,
}
impl ValidationError {
pub fn field(name: &'static str) -> Self {
Self {
name,
value: String::new(),
reason: String::new(),
}
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self
}
pub fn reason(mut self, reason: impl Into<String>) -> Self {
self.reason = reason.into();
self
}
pub fn into_parse_error<'v>(self) -> Error<'static, 'v> {
Error::InvalidValue(Box::new(InvalidValue {
name: self.name,
value: self.value,
reason: self.reason,
}))
}
}
pub fn as_str(value: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(value)
}
pub const fn table_len<T>(groups: &[&[T]]) -> usize {
let mut total = 0;
let mut i = 0;
while i < groups.len() {
total += groups[i].len();
i += 1;
}
total
}
pub const fn concat_flags<const N: usize>(
groups: &[&[&'static Flag<'static>]],
) -> [&'static Flag<'static>; N] {
static PLACEHOLDER: Flag<'static> = Flag::BOOL;
let mut out = [&PLACEHOLDER; N];
let mut at = 0;
let mut g = 0;
while g < groups.len() {
let group = groups[g];
let mut i = 0;
while i < group.len() {
out[at] = group[i];
at += 1;
i += 1;
}
g += 1;
}
assert!(
at == N,
"`N` must be `table_len` of the same groups, or the table would keep a placeholder \
that answers to nothing"
);
out
}
pub const fn concat_args<const N: usize>(
groups: &[&[&'static Arg<'static>]],
) -> [&'static Arg<'static>; N] {
static PLACEHOLDER: Arg<'static> = Arg::REQUIRED;
let mut out = [&PLACEHOLDER; N];
let mut at = 0;
let mut g = 0;
while g < groups.len() {
let group = groups[g];
let mut i = 0;
while i < group.len() {
out[at] = group[i];
at += 1;
i += 1;
}
g += 1;
}
assert!(
at == N,
"`N` must be `table_len` of the same groups, or the table would keep a placeholder \
that answers to nothing"
);
out
}
pub const HELP_LONG_KEY: u64 = u64::MAX;
pub const HELP_SHORT_KEY: u64 = u64::MAX - 1;
pub static HELP_LONG: Flag<'static> = Flag {
key: HELP_LONG_KEY,
name: "help",
longs: &["help"],
action: ArgAction::HelpLong,
..Flag::BOOL
};
pub const VERSION_LONG_KEY: u64 = u64::MAX - 2;
pub const VERSION_SHORT_KEY: u64 = u64::MAX - 3;
pub static VERSION_LONG: Flag<'static> = Flag {
key: VERSION_LONG_KEY,
name: "version",
longs: &["version"],
action: ArgAction::Version,
..Flag::BOOL
};
pub static VERSION_SHORT: Flag<'static> = Flag {
key: VERSION_SHORT_KEY,
name: "version",
shorts: b"V",
action: ArgAction::Version,
..Flag::BOOL
};
pub static HELP_SHORT: Flag<'static> = Flag {
key: HELP_SHORT_KEY,
name: "help",
shorts: b"h",
action: ArgAction::HelpShort,
..Flag::BOOL
};
pub(crate) fn find_named<'t>(cmd: &'t Command<'t>, name: &[u8]) -> Option<&'t Command<'t>> {
let subcommands = || cmd.subcommands.iter().copied();
subcommands()
.find(|c| c.name.as_bytes() == name)
.or_else(|| subcommands().find(|c| c.aliases.iter().any(|a| a.as_bytes() == name)))
}
#[cfg(feature = "diagnostics")]
pub fn render_failure(spec: &spec::Spec<'_>, argv: &[&OsStr], error: &Error<'_, '_>) -> String {
diagnostic::render(spec, argv, error, diagnostic::Style::auto())
}
#[cfg(feature = "diagnostics")]
pub fn render_failure_plain(
spec: &spec::Spec<'_>,
argv: &[&OsStr],
error: &Error<'_, '_>,
) -> String {
diagnostic::render(spec, argv, error, diagnostic::Style::PLAIN)
}
#[cfg(feature = "diagnostics")]
pub fn render_failure_view<'a>(
spec: &'a spec::Spec<'a>,
argv: &[&OsStr],
error: &Error<'_, '_>,
view: &'a spec::ViewMeta<'a>,
) -> String {
diagnostic::render_view(spec, argv, error, diagnostic::Style::auto(), view)
}
#[cfg(all(feature = "spec", not(feature = "diagnostics")))]
pub fn render_failure(spec: &spec::Spec<'_>, argv: &[&OsStr], error: &Error<'_, '_>) -> String {
let _ = (spec, argv);
::std::format!("error: {error:?}\n")
}
#[cfg(all(feature = "spec", not(feature = "diagnostics")))]
pub fn render_failure_plain(
spec: &spec::Spec<'_>,
argv: &[&OsStr],
error: &Error<'_, '_>,
) -> String {
render_failure(spec, argv, error)
}
#[cfg(all(feature = "spec", not(feature = "diagnostics")))]
pub fn render_failure_view(
spec: &spec::Spec<'_>,
argv: &[&OsStr],
error: &Error<'_, '_>,
view: &spec::ViewMeta<'_>,
) -> String {
let _ = (spec, argv, view);
::std::format!("error: {error:?}\n")
}
#[cfg(feature = "diagnostics")]
pub fn render_warnings(warnings: &[warn::Warning<'_>]) -> String {
diagnostic::render_warnings(warnings, diagnostic::Style::auto())
}
#[cfg(all(feature = "spec", not(feature = "diagnostics")))]
pub fn render_warnings(warnings: &[warn::Warning<'_>]) -> String {
warn::render_warnings(warnings)
}
pub const SPEC_REQUEST: &str = "__usage_spec__";
pub fn is_spec_request(root: &Command<'_>, argv: &[&OsStr]) -> bool {
let [first, ..] = argv else { return false };
first.as_encoded_bytes() == SPEC_REQUEST.as_bytes()
&& find_named(root, SPEC_REQUEST.as_bytes()).is_none()
}
pub fn is_help_flag(flag: &Flag<'_>) -> bool {
matches!(
flag.action,
ArgAction::Help | ArgAction::HelpShort | ArgAction::HelpLong | ArgAction::HelpAll
)
}
pub fn is_version_flag(flag: &Flag<'_>) -> bool {
flag.action == ArgAction::Version
}
pub fn is_version_arg(cmd: &Command<'_>, word: &OsStr) -> bool {
let token = word.as_encoded_bytes();
if let Some(long) = token.strip_prefix(b"--") {
if let Some(flag) = cmd.flags.iter().find(|flag| {
flag.longs
.iter()
.any(|spelling| spelling.as_bytes() == long)
}) {
return is_version_flag(flag);
}
if cmd.flags.iter().any(|flag| {
flag.negate
.is_some_and(|spelling| spelling.as_bytes() == long)
}) {
return false;
}
return long == b"version" && cmd.version && !cmd.disable_version_flag;
}
if let [b'-', short] = token {
if let Some(flag) = cmd.flags.iter().find(|flag| flag.shorts.contains(short)) {
return is_version_flag(flag);
}
return *short == b'V' && cmd.version && !cmd.disable_version_flag;
}
false
}
pub const fn find_subcommand<'a>(
subcommands: &'a [&'a Command<'a>],
name: &str,
) -> &'a Command<'a> {
let mut i = 0;
while i < subcommands.len() {
if str_eq(subcommands[i].name, name) {
return subcommands[i];
}
i += 1;
}
let mut i = 0;
while i < subcommands.len() {
let candidate = subcommands[i];
let mut a = 0;
while a < candidate.aliases.len() {
if str_eq(candidate.aliases[a], name) {
return candidate;
}
a += 1;
}
i += 1;
}
panic!("`default_subcommand` names a command that this one does not have")
}
pub const fn assert_unique_subcommand_names(subcommands: &[&Command<'_>]) {
const fn form<'a>(cmd: &'a Command<'a>, at: usize) -> Option<&'a str> {
if at == 0 {
Some(cmd.name)
} else if at <= cmd.aliases.len() {
Some(cmd.aliases[at - 1])
} else {
None
}
}
let mut command = 0;
while command < subcommands.len() {
let mut at = 0;
while let Some(name) = form(subcommands[command], at) {
let mut other_command = command;
while other_command < subcommands.len() {
let mut other_at = if other_command == command { at + 1 } else { 0 };
while let Some(other) = form(subcommands[other_command], other_at) {
assert!(
!str_eq(name, other),
"two subcommands answer to the same name, counting aliases"
);
other_at += 1;
}
other_command += 1;
}
at += 1;
}
command += 1;
}
}
const fn str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
pub fn os_string_from_bytes(value: Vec<u8>) -> Result<OsString, Vec<u8>> {
#[cfg(unix)]
{
Ok(std::os::unix::ffi::OsStringExt::from_vec(value))
}
#[cfg(not(unix))]
{
match String::from_utf8(value) {
Ok(text) => Ok(OsString::from(text)),
Err(bad) => Err(bad.into_bytes()),
}
}
}
#[cold]
#[inline(never)]
pub(crate) fn invalid_value_error<'t, 'v>(
name: &'t str,
value: String,
reason: String,
) -> Error<'t, 'v> {
Error::InvalidValue(Box::new(InvalidValue {
name,
value,
reason,
}))
}
#[cold]
#[inline(never)]
pub fn invalid_utf8_value<'t, 'v>(name: &'t str, bad: std::string::FromUtf8Error) -> Error<'t, 'v> {
invalid_value_error(
name,
String::from_utf8_lossy(bad.as_bytes()).into_owned(),
bad.utf8_error().to_string(),
)
}
#[cold]
#[inline(never)]
pub fn invalid_parsed_value<'t, 'v>(
name: &'t str,
value: String,
reason: &dyn std::fmt::Display,
) -> Error<'t, 'v> {
invalid_value_error(name, value, reason.to_string())
}
#[cold]
#[inline(never)]
pub fn invalid_choice_value<'t, 'v>(name: &'t str, value: String) -> Error<'t, 'v> {
invalid_value_error(name, value, String::from("not one of the declared values"))
}
#[cold]
#[inline(never)]
pub fn invalid_os_value<'t, 'v>(name: &'t str, bytes: Vec<u8>) -> Error<'t, 'v> {
invalid_value_error(
name,
String::from_utf8_lossy(&bytes).into_owned(),
"this platform cannot hold these bytes in a path".to_string(),
)
}
#[inline]
pub fn utf8_values<'t, 'v>(
values: Vec<Vec<u8>>,
name: &'t str,
) -> Result<Vec<String>, Error<'t, 'v>> {
if values.is_empty() {
return Ok(Vec::new());
}
utf8_values_given(values, name)
}
#[inline(never)]
fn utf8_values_given<'t, 'v>(
values: Vec<Vec<u8>>,
name: &'t str,
) -> Result<Vec<String>, Error<'t, 'v>> {
let mut out = Vec::with_capacity(values.len());
for value in values {
match String::from_utf8(value) {
Ok(text) => out.push(text),
Err(bad) => return Err(invalid_utf8_value(name, bad)),
}
}
Ok(out)
}
#[inline]
pub fn parsed_values<'t, 'v, T>(
values: Vec<Vec<u8>>,
name: &'t str,
) -> Result<Vec<T>, Error<'t, 'v>>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
if values.is_empty() {
return Ok(Vec::new());
}
parsed_values_given(values, name)
}
#[inline(never)]
fn parsed_values_given<'t, 'v, T>(
values: Vec<Vec<u8>>,
name: &'t str,
) -> Result<Vec<T>, Error<'t, 'v>>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let mut out = Vec::with_capacity(values.len());
for value in values {
let text = match String::from_utf8(value) {
Ok(text) => text,
Err(bad) => return Err(invalid_utf8_value(name, bad)),
};
match text.parse() {
Ok(parsed) => out.push(parsed),
Err(reason) => return Err(invalid_parsed_value(name, text, &reason)),
}
}
Ok(out)
}
#[inline]
pub fn os_values<'t, 'v, T: From<OsString>>(
values: Vec<Vec<u8>>,
name: &'t str,
) -> Result<Vec<T>, Error<'t, 'v>> {
if values.is_empty() {
return Ok(Vec::new());
}
os_values_given(values, name)
}
#[inline(never)]
fn os_values_given<'t, 'v, T: From<OsString>>(
values: Vec<Vec<u8>>,
name: &'t str,
) -> Result<Vec<T>, Error<'t, 'v>> {
let mut out = Vec::with_capacity(values.len());
for value in values {
match os_string_from_bytes(value) {
Ok(os) => out.push(T::from(os)),
Err(bytes) => return Err(invalid_os_value(name, bytes)),
}
}
Ok(out)
}
pub struct Parser<'t, 'a, 'v> {
argv: &'a [&'v OsStr],
pos: usize,
cmd: &'t Command<'t>,
#[cfg(feature = "spec")]
root: &'t Command<'t>,
#[cfg(feature = "spec")]
view: Option<&'t spec::ViewMeta<'t>>,
unknown_flags: UnknownFlags,
dont_delimit_trailing_values: bool,
ancestors: [Option<&'t Command<'t>>; MAX_DEPTH],
depth: usize,
bundle: &'v [u8],
bundle_token: &'v [u8],
collecting: Option<&'t Flag<'t>>,
cmd_start: usize,
starts: [usize; MAX_DEPTH],
collected: u32,
arg_pos: usize,
arg_taken: u32,
arg_filled: bool,
command_arg_found: bool,
flags_stopped: bool,
separator_seen: bool,
default_taken: bool,
done: bool,
action_errors: bool,
help_span: (usize, usize),
}
impl<'t: 'v, 'a, 'v> Parser<'t, 'a, 'v> {
pub fn new(root: &'t Command<'t>, argv: &'a [&'v OsStr]) -> Self {
Self::with_action_errors(root, argv, true)
}
#[cfg(feature = "complete")]
pub(crate) fn for_completion(root: &'t Command<'t>, argv: &'a [&'v OsStr]) -> Self {
Self::with_action_errors(root, argv, false)
}
fn with_action_errors(
root: &'t Command<'t>,
argv: &'a [&'v OsStr],
action_errors: bool,
) -> Self {
Parser {
argv,
pos: 0,
cmd: root,
#[cfg(feature = "spec")]
root,
#[cfg(feature = "spec")]
view: None,
unknown_flags: match root.unknown_flags {
::core::option::Option::Some(mode) => mode,
::core::option::Option::None => UnknownFlags::Value,
},
dont_delimit_trailing_values: root.dont_delimit_trailing_values,
ancestors: [None; MAX_DEPTH],
depth: 0,
bundle: &[],
bundle_token: &[],
collecting: None,
cmd_start: 0,
starts: [0; MAX_DEPTH],
collected: 0,
arg_pos: 0,
arg_taken: 0,
arg_filled: false,
command_arg_found: false,
flags_stopped: false,
separator_seen: false,
default_taken: false,
done: false,
action_errors,
help_span: (0, 0),
}
}
#[cfg(feature = "spec")]
pub fn with_view(mut self, view: &'t spec::ViewMeta<'t>) -> Self {
self.view = Some(view);
self
}
pub fn command(&self) -> &'t Command<'t> {
self.cmd
}
pub fn double_dash_seen(&self) -> bool {
self.separator_seen
}
pub fn command_path(&self) -> Vec<(&'t Command<'t>, usize)> {
let mut out = Vec::with_capacity(self.depth + 1);
for (i, ancestor) in self.ancestors[..self.depth].iter().enumerate() {
if let Some(cmd) = ancestor {
out.push((*cmd, self.starts[i]));
}
}
out.push((self.cmd, self.cmd_start));
out
}
pub fn help_span(&self) -> (usize, usize) {
self.help_span
}
pub fn command_start(&self) -> usize {
self.cmd_start
}
pub fn flags_stopped(&self) -> bool {
self.flags_stopped
}
pub fn collecting(&self) -> Option<&'t Flag<'t>> {
self.collecting
}
pub fn pending_arg(&self) -> Option<&'t Arg<'t>> {
self.next_arg()
}
pub fn flags_in_scope(&self) -> impl Iterator<Item = &'t Flag<'t>> + '_ {
self.in_scope()
}
#[allow(clippy::should_implement_trait)] pub fn next_event(&mut self) -> Option<Result<Event<'t, 'a, 'v>, Error<'t, 'v>>> {
if self.done {
return None;
}
let event = self.step();
if matches!(event, Some(Ok(Event::Flag { .. } | Event::Arg { .. }))) {
self.command_arg_found = true;
}
if let Some(Err(_)) = event {
self.done = true;
}
event
}
fn step(&mut self) -> Option<Result<Event<'t, 'a, 'v>, Error<'t, 'v>>> {
if !self.bundle.is_empty() {
return Some(self.short_flag());
}
if self.cmd.subcommand_precedence_over_arg && !self.flags_stopped {
if let Some(token) = self.argv.get(self.pos).map(bytes) {
if let Some(sub) = self.find_subcommand(token) {
if self.cmd.args_conflicts_with_subcommands && self.command_arg_found {
return Some(Err(Error::SubcommandConflict { subcommand: sub }));
}
self.pos += 1;
return Some(self.descend(sub).map(|()| Event::Command(sub)));
}
}
}
if let Some(flag) = self.collecting {
match self.argv.get(self.pos) {
Some(next)
if flag
.value_terminator
.is_some_and(|terminator| bytes(next) == terminator) =>
{
self.pos += 1;
self.collecting = None;
return self.step();
}
Some(next)
if (!is_flag_like(bytes(next))
|| (flag.allow_negative_numbers && is_negative_number(bytes(next))))
&& bytes(next) != b"--" =>
{
self.pos += 1;
self.collected += values_in(bytes(next), flag.delimiter);
if flag.var_max.is_some_and(|max| self.collected >= max) {
self.collecting = None;
}
if let Some(max) = flag.var_max.filter(|max| self.collected > *max) {
return Some(Err(Error::VarTooMany {
name: flag.name,
max: max as usize,
got: self.collected as usize,
}));
}
return Some(Ok(Event::Flag {
flag,
value: Some(bytes(next)),
negated: false,
}));
}
Some(_) => self.collecting = None,
None => {}
}
}
let token = bytes(self.argv.get(self.pos)?);
self.pos += 1;
if self.flags_stopped && (token != b"--" || self.separator_seen) {
return Some(self.word(token));
}
if token == b"--" {
if self
.next_arg()
.is_some_and(|a| a.double_dash == DoubleDash::Preserve)
{
return Some(self.word(token));
}
self.flags_stopped = true;
self.separator_seen = true;
if let Some(idx) = self.cmd.args[self.arg_pos..]
.iter()
.position(|a| a.double_dash == DoubleDash::Required)
{
self.arg_pos += idx;
self.arg_taken = 0;
}
return self.step();
}
if self.arg_taken > 0
&& self.next_arg().is_some_and(|arg| {
arg.value_terminator
.is_some_and(|terminator| token == terminator)
})
{
self.advance_arg();
return self.step();
}
let declared_numeric_short = matches!(token, [b'-', short]
if short.is_ascii_digit() && self.find_short(*short).is_some());
if !declared_numeric_short
&& is_negative_number(token)
&& self
.next_arg()
.is_some_and(|arg| arg.allow_negative_numbers)
{
return Some(self.word(token));
}
if !declared_numeric_short
&& is_negative_number(token)
&& self.cmd.external_subcommand
&& !self.arg_filled
{
return Some(self.word(token));
}
if is_flag_like(token) {
if token.starts_with(b"--") {
return Some(self.long_flag(token));
}
match self.check_bundle(token) {
Ok(()) => {}
Err(e) if self.unknown_flags == UnknownFlags::Error => {
return Some(Err(e));
}
Err(_) => return Some(self.word(token)),
}
self.bundle = &token[1..];
self.bundle_token = token;
return Some(self.short_flag());
}
Some(self.word(token))
}
fn long_flag(&mut self, token: &'v [u8]) -> Result<Event<'t, 'a, 'v>, Error<'t, 'v>> {
let body = &token[2..];
let (name, attached) = match body.iter().position(|&b| b == b'=') {
Some(i) => (&body[..i], Some(&body[i + 1..])),
None => (body, None),
};
if let Some(flag) = self.find_long(name) {
let value = if flag.takes_value {
match attached {
Some(v) => Some(v),
None => self.take_detached_value(flag)?,
}
} else if flag.bool_value {
validate_bool_value(flag, attached)?
} else {
None
};
if flag.variadic {
if let Some(value) = value {
self.start_collecting(flag, value)?;
}
}
if let Some(error) = self.flag_action(flag, true) {
return Err(error);
}
return Ok(Event::Flag {
flag,
value,
negated: false,
});
}
if let Some(flag) = self.find_negation(name) {
return Ok(Event::Flag {
flag,
value: if flag.bool_value {
validate_bool_value(flag, attached)?
} else {
None
},
negated: true,
});
}
let version_command = self.version_command();
if name == b"version" && version_command.version && !version_command.disable_version_flag {
return Ok(Event::Flag {
flag: &VERSION_LONG,
value: None,
negated: false,
});
}
if name == b"help" && !self.cmd.disable_help_flag {
return Ok(Event::Flag {
flag: &HELP_LONG,
value: None,
negated: false,
});
}
if self.unknown_flags == UnknownFlags::Error {
return Err(Error::UnknownFlag { token });
}
self.word(token)
}
fn check_bundle(&self, token: &'v [u8]) -> Result<(), Error<'t, 'v>> {
let mut rest = &token[1..];
while let Some((&byte, tail)) = rest.split_first() {
match self.find_short(byte) {
None => return Err(Error::UnknownFlag { token }),
Some(flag) if flag.takes_value => return Ok(()),
Some(_) => rest = tail,
}
}
Ok(())
}
fn short_flag(&mut self) -> Result<Event<'t, 'a, 'v>, Error<'t, 'v>> {
let byte = self.bundle[0];
let rest = &self.bundle[1..];
let Some(flag) = self.find_short(byte) else {
self.bundle = &[];
return Err(Error::UnknownFlag {
token: self.bundle_token,
});
};
if !flag.takes_value {
self.bundle = rest;
if let Some(error) = self.flag_action(flag, false) {
self.bundle = &[];
return Err(error);
}
return Ok(Event::Flag {
flag,
value: None,
negated: false,
});
}
self.bundle = &[];
let value = if rest.is_empty() {
self.take_detached_value(flag)?
} else if rest[0] == b'=' {
Some(&rest[1..])
} else {
Some(rest)
};
if flag.variadic {
if let Some(value) = value {
self.start_collecting(flag, value)?;
}
}
if let Some(error) = self.flag_action(flag, false) {
return Err(error);
}
Ok(Event::Flag {
flag,
value,
negated: false,
})
}
fn flag_action(&self, flag: &'t Flag<'t>, long_spelling: bool) -> Option<Error<'t, 'v>> {
if matches!(
flag.key,
HELP_LONG_KEY | HELP_SHORT_KEY | VERSION_LONG_KEY | VERSION_SHORT_KEY
) || !self.action_errors
{
return None;
}
match flag.action {
ArgAction::Set => None,
ArgAction::Help => Some(Error::Help {
cmd: self.cmd,
long: long_spelling,
}),
ArgAction::HelpShort => Some(Error::Help {
cmd: self.cmd,
long: false,
}),
ArgAction::HelpLong => Some(Error::Help {
cmd: self.cmd,
long: true,
}),
ArgAction::HelpAll => Some(Error::HelpAll { cmd: self.cmd }),
ArgAction::Version => Some(Error::Version {
long: long_spelling,
}),
}
}
fn take_detached_value(
&mut self,
flag: &'t Flag<'t>,
) -> Result<Option<&'v [u8]>, Error<'t, 'v>> {
if flag.require_equals {
return self.missing_or_default(flag);
}
match self.argv.get(self.pos) {
Some(next)
if flag.allow_hyphen_values
|| !is_flag_like(bytes(next))
|| (flag.allow_negative_numbers && is_negative_number(bytes(next))) =>
{
self.pos += 1;
Ok(Some(bytes(next)))
}
_ => self.missing_or_default(flag),
}
}
fn missing_or_default(&self, flag: &'t Flag<'t>) -> Result<Option<&'v [u8]>, Error<'t, 'v>> {
match flag.default_missing {
Some(value) => Ok(Some(value)),
None if flag.value_optional => Ok(None),
None => Err(Error::MissingFlagValue { flag }),
}
}
fn word(&mut self, token: &'v [u8]) -> Result<Event<'t, 'a, 'v>, Error<'t, 'v>> {
if !self.arg_filled && !self.flags_stopped {
if let Some(sub) = self.find_subcommand(token) {
if self.cmd.args_conflicts_with_subcommands && self.command_arg_found {
return Err(Error::SubcommandConflict { subcommand: sub });
}
self.descend(sub)?;
return Ok(Event::Command(sub));
}
if token == b"help"
&& !self.cmd.disable_help_subcommand
&& !self.cmd.subcommands.is_empty()
{
let mut cmd = self.cmd;
let from = self.pos;
while let Some(next) = self.argv.get(self.pos) {
let Some(sub) = find_named(cmd, bytes(next)) else {
break;
};
cmd = sub;
self.pos += 1;
}
self.help_span = (from, self.pos);
return Err(Error::Help { cmd, long: true });
}
if let Some(default) = self.cmd.default_subcommand {
let default_accepts_negative = is_negative_number(token)
&& default
.args
.first()
.is_some_and(|arg| arg.allow_negative_numbers);
if !self.default_taken
&& (!is_flag_like(token) || default_accepts_negative)
&& token != b"--"
&& token != b"-"
{
self.default_taken = true;
self.descend(default)?;
self.pos -= 1;
self.cmd_start = self.pos;
return Ok(Event::Command(default));
}
}
if self.cmd.external_subcommand
&& (!is_flag_like(token) || is_negative_number(token))
&& token != b"--"
&& token != b"-"
{
let from = self.pos - 1;
self.pos = self.argv.len();
return Ok(Event::External {
values: &self.argv[from..],
});
}
}
self.reserve_for_required_positionals();
let Some(arg) = self.next_arg() else {
return Err(Error::UnexpectedArg { token });
};
if arg.double_dash == DoubleDash::Required && !self.separator_seen {
return Err(Error::ArgRequiresDoubleDash { arg });
}
self.arg_filled = true;
let trailing_value = self.separator_seen || arg.double_dash == DoubleDash::Automatic;
let delimit = !(self.dont_delimit_trailing_values && trailing_value);
if arg.double_dash == DoubleDash::Automatic {
self.flags_stopped = true;
}
if arg.var {
self.arg_taken += values_in(token, delimit.then_some(arg.delimiter).flatten());
if let Some(max) = arg.var_max.filter(|max| self.arg_taken > *max) {
return Err(Error::VarTooMany {
name: arg.name,
max: max as usize,
got: self.arg_taken as usize,
});
}
if arg.var_max.is_some_and(|max| self.arg_taken >= max) {
self.advance_arg();
}
} else {
self.advance_arg();
}
Ok(Event::Arg {
arg,
value: token,
delimit,
})
}
fn descend(&mut self, sub: &'t Command<'t>) -> Result<(), Error<'t, 'v>> {
if self.depth >= MAX_DEPTH {
return Err(Error::TooDeep);
}
self.ancestors[self.depth] = Some(self.cmd);
self.starts[self.depth] = self.cmd_start;
self.depth += 1;
self.cmd = sub;
if let ::core::option::Option::Some(mode) = sub.unknown_flags {
self.unknown_flags = mode;
}
self.dont_delimit_trailing_values |= sub.dont_delimit_trailing_values;
self.cmd_start = self.pos;
self.arg_pos = 0;
self.arg_taken = 0;
self.arg_filled = false;
self.command_arg_found = false;
Ok(())
}
fn advance_arg(&mut self) {
self.arg_pos += 1;
self.arg_taken = 0;
}
fn start_collecting(&mut self, flag: &'t Flag<'t>, first: &[u8]) -> Result<(), Error<'t, 'v>> {
self.collected = values_in(first, flag.delimiter);
if let Some(max) = flag.var_max.filter(|max| self.collected > *max) {
return Err(Error::VarTooMany {
name: flag.name,
max: max as usize,
got: self.collected as usize,
});
}
self.collecting = if flag.var_max.is_some_and(|max| self.collected >= max) {
None
} else {
Some(flag)
};
Ok(())
}
fn next_arg(&self) -> Option<&'t Arg<'t>> {
self.cmd.args.get(self.arg_pos).copied()
}
fn reserve_for_required_positionals(&mut self) {
if !self.cmd.allow_missing_positional || self.arg_taken != 0 {
return;
}
loop {
let Some(current) = self.next_arg() else {
return;
};
if current.required {
return;
}
let required_after = self.cmd.args[self.arg_pos + 1..]
.iter()
.filter(|arg| arg.required)
.count();
if required_after == 0 {
return;
}
let remaining_values = 1 + self.argv[self.pos..]
.iter()
.filter(|word| self.flags_stopped || !is_flag_like(bytes(word)))
.count();
if remaining_values > required_after {
return;
}
self.advance_arg();
}
}
#[cfg(feature = "spec")]
fn view_allows_own_flag(&self, flag: &Flag<'_>) -> bool {
match self.view {
None => true,
Some(view) => {
!core::ptr::eq(self.cmd, self.root) || is_version_flag(flag) || view.carries(flag)
}
}
}
#[cfg(not(feature = "spec"))]
fn view_allows_own_flag(&self, _flag: &Flag<'_>) -> bool {
true
}
#[cfg(feature = "spec")]
fn view_allows_inherited_flag(&self, flag: &Flag<'_>) -> bool {
match self.view {
None => true,
Some(view) => {
self.root
.flags
.iter()
.any(|root| core::ptr::eq(*root, flag))
&& (is_version_flag(flag) || view.carries(flag))
}
}
}
#[cfg(not(feature = "spec"))]
fn view_allows_inherited_flag(&self, _flag: &Flag<'_>) -> bool {
true
}
#[cfg(feature = "spec")]
fn inherited_flag_is_in_scope(&self, flag: &Flag<'_>) -> bool {
flag.global || (self.view.is_some() && is_version_flag(flag))
}
#[cfg(not(feature = "spec"))]
fn inherited_flag_is_in_scope(&self, flag: &Flag<'_>) -> bool {
flag.global
}
fn in_scope(&self) -> impl Iterator<Item = &'t Flag<'t>> + '_ {
let own = self
.cmd
.flags
.iter()
.copied()
.filter(|flag| self.view_allows_own_flag(flag));
let inherited = self.ancestors[..self.depth]
.iter()
.rev()
.filter_map(|c| *c)
.flat_map(|c| c.flags.iter().copied())
.filter(|flag| self.inherited_flag_is_in_scope(flag))
.filter(|flag| self.view_allows_inherited_flag(flag));
own.chain(inherited)
}
fn find_long(&self, name: &[u8]) -> Option<&'t Flag<'t>> {
self.in_scope()
.find(|f| f.longs.iter().any(|l| l.as_bytes() == name))
}
fn find_negation(&self, name: &[u8]) -> Option<&'t Flag<'t>> {
self.in_scope()
.find(|f| f.negate.is_some_and(|n| n.as_bytes() == name))
}
fn find_short(&self, byte: u8) -> Option<&'t Flag<'t>> {
self.in_scope()
.find(|f| f.shorts.contains(&byte))
.or(if byte == b'h' && !self.cmd.disable_help_flag {
Some(&HELP_SHORT)
} else if byte == b'V'
&& self.version_command().version
&& !self.version_command().disable_version_flag
{
Some(&VERSION_SHORT)
} else {
None
})
}
#[cfg(feature = "spec")]
fn version_command(&self) -> &'t Command<'t> {
if self.view.is_some() {
self.root
} else {
self.cmd
}
}
#[cfg(not(feature = "spec"))]
fn version_command(&self) -> &'t Command<'t> {
self.cmd
}
fn find_subcommand(&self, name: &[u8]) -> Option<&'t Command<'t>> {
find_named(self.cmd, name)
}
}
fn bytes<'v>(s: &&'v OsStr) -> &'v [u8] {
s.as_encoded_bytes()
}
fn values_in(word: &[u8], delimiter: ::core::option::Option<u8>) -> u32 {
match delimiter {
Some(d) => 1 + word.iter().filter(|b| **b == d).count() as u32,
None => 1,
}
}
fn is_flag_like(token: &[u8]) -> bool {
matches!(token, [b'-', rest @ ..] if !rest.is_empty())
}
fn is_negative_number(token: &[u8]) -> bool {
token.strip_prefix(b"-").is_some_and(is_number)
}
fn is_number(rest: &[u8]) -> bool {
let (mantissa, exponent) = match rest.iter().position(|b| matches!(b, b'e' | b'E')) {
Some(at) => (&rest[..at], Some(&rest[at + 1..])),
None => (rest, None),
};
let mut seen_digit = false;
let mut seen_dot = false;
for &b in mantissa {
match b {
b'0'..=b'9' => seen_digit = true,
b'.' if !seen_dot => seen_dot = true,
_ => return false,
}
}
if !seen_digit {
return false;
}
match exponent {
None => true,
Some(exp) => {
let digits = exp
.strip_prefix(b"+")
.or_else(|| exp.strip_prefix(b"-"))
.unwrap_or(exp);
!digits.is_empty() && digits.iter().all(|b| b.is_ascii_digit())
}
}
}
fn validate_bool_value<'t, 'v>(
flag: &'t Flag<'t>,
value: Option<&'v [u8]>,
) -> Result<Option<&'v [u8]>, Error<'t, 'v>> {
match value {
None | Some(b"true" | b"false") => Ok(value),
Some(_) => Err(Error::InvalidChoice {
name: flag.name,
choices: &["true", "false"],
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
static FORCE: Flag = Flag {
key: 1,
longs: &["force"],
shorts: b"f",
..Flag::BOOL
};
static EXPLICIT_BOOL: Flag = Flag {
key: 20,
name: "color",
longs: &["color"],
negate: Some("no-color"),
bool_value: true,
..Flag::BOOL
};
static EXPLICIT_BOOL_ROOT: Command = Command {
name: "ex",
flags: &[&EXPLICIT_BOOL],
..Command::EMPTY
};
static JOBS: Flag = Flag {
key: 2,
longs: &["jobs"],
shorts: b"j",
allow_negative_numbers: true,
..Flag::VALUE
};
static COLOR: Flag = Flag {
key: 3,
longs: &["color"],
negate: Some("no-color"),
..Flag::BOOL
};
static VERBOSE: Flag = Flag {
key: 4,
longs: &["verbose"],
shorts: b"v",
global: true,
..Flag::BOOL
};
static FILE: Arg = Arg {
key: 10,
name: "file",
allow_negative_numbers: true,
..Arg::REQUIRED
};
static REST: Arg = Arg {
key: 11,
name: "rest",
..Arg::VAR
};
static INSTALL: Command = Command {
name: "install",
aliases: &["i"],
flags: &[&FORCE],
key: 100,
..Command::EMPTY
};
static STRICT_INSTALL: Command = Command {
name: "install",
aliases: &["i"],
flags: &[&FORCE],
key: 100,
..Command::EMPTY
};
static STRICT: Command = Command {
name: "ex",
flags: &[&FORCE, &JOBS, &COLOR, &VERBOSE],
args: &[&FILE, &REST],
subcommands: &[&STRICT_INSTALL],
unknown_flags: Some(UnknownFlags::Error),
..Command::EMPTY
};
static ROOT: Command = Command {
name: "ex",
flags: &[&FORCE, &JOBS, &COLOR, &VERBOSE],
args: &[&FILE, &REST],
subcommands: &[&INSTALL],
..Command::EMPTY
};
static ARGUMENT_CONFLICT: Command = Command {
name: "ex",
flags: &[&FORCE],
subcommands: &[&INSTALL],
args_conflicts_with_subcommands: true,
..Command::EMPTY
};
static TASK: Arg = Arg {
key: 20,
name: "task",
..Arg::REQUIRED
};
static RUN_TASK: Arg = Arg {
key: 21,
name: "run_task",
..Arg::REQUIRED
};
static DEEP: Command = Command {
name: "deep",
args: &[&RUN_TASK],
key: 203,
..Command::EMPTY
};
static LINT: Command = Command {
name: "lint",
subcommands: &[&DEEP],
default_subcommand: Some(&DEEP),
key: 202,
..Command::EMPTY
};
static RUN: Command = Command {
name: "run",
args: &[&RUN_TASK],
subcommands: &[&LINT],
key: 200,
..Command::EMPTY
};
static DEFAULTING: Command = Command {
name: "mise",
flags: &[&VERBOSE],
args: &[&TASK],
subcommands: &[&RUN, &INSTALL],
default_subcommand: Some(find_subcommand(&[&RUN, &INSTALL], "run")),
..Command::EMPTY
};
fn parse<'t: 'v, 'v>(
root: &'t Command<'t>,
argv: &'v [&'v OsStr],
) -> Result<Vec<Event<'t, 'v, 'v>>, Error<'t, 'v>> {
let mut parser = Parser::new(root, argv);
let mut events = Vec::new();
while let Some(event) = parser.next_event() {
events.push(event?);
}
Ok(events)
}
fn argv<const N: usize>(tokens: [&str; N]) -> [&OsStr; N] {
tokens.map(OsStr::new)
}
#[test]
fn long_boolean() {
let a = argv(["--force"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Flag {
flag: &FORCE,
value: None,
negated: false
}]
);
}
#[test]
fn long_boolean_accepts_only_opted_in_attached_values() {
for (token, negated, value) in [
("--color=false", false, b"false".as_slice()),
("--color=true", false, b"true".as_slice()),
("--no-color=false", true, b"false".as_slice()),
] {
let a = argv([token]);
assert_eq!(
parse(&EXPLICIT_BOOL_ROOT, &a).unwrap(),
vec![Event::Flag {
flag: &EXPLICIT_BOOL,
value: Some(value),
negated,
}]
);
}
let a = argv(["--color=maybe"]);
assert!(matches!(
parse(&EXPLICIT_BOOL_ROOT, &a),
Err(Error::InvalidChoice { name: "color", .. })
));
let a = argv(["--force=false"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Flag {
flag: &FORCE,
value: None,
negated: false,
}]
);
}
#[test]
fn long_value_forms() {
for tokens in [vec!["--jobs=8"], vec!["--jobs", "8"]] {
let a: Vec<&OsStr> = tokens.iter().map(|t| OsStr::new(*t)).collect();
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Flag {
flag: &JOBS,
value: Some(b"8"),
negated: false
}],
"{tokens:?}"
);
}
}
#[test]
fn long_value_keeps_later_equals() {
let a = argv(["--jobs=a=b"]);
let Event::Flag { value, .. } = parse(&ROOT, &a).unwrap()[0] else {
panic!("expected a flag");
};
assert_eq!(value, Some(&b"a=b"[..]));
}
#[test]
fn long_value_attached_empty_is_empty_not_absent() {
let a = argv(["--jobs="]);
let Event::Flag { value, .. } = parse(&ROOT, &a).unwrap()[0] else {
panic!("expected a flag");
};
assert_eq!(value, Some(&b""[..]));
}
#[test]
fn long_value_refuses_flaglike_next_word() {
let a = argv(["--jobs", "--force"]);
assert_eq!(
parse(&ROOT, &a),
Err(Error::MissingFlagValue { flag: &JOBS })
);
}
#[test]
fn long_value_accepts_negative_number() {
let a = argv(["--jobs", "-1"]);
let Event::Flag { value, .. } = parse(&ROOT, &a).unwrap()[0] else {
panic!("expected a flag");
};
assert_eq!(value, Some(&b"-1"[..]));
}
#[test]
fn missing_optional_positional_reserves_the_last_word() {
static OPTIONAL: Arg = Arg {
key: 90,
name: "optional",
required: false,
..Arg::REQUIRED
};
static REQUIRED: Arg = Arg {
key: 91,
name: "required",
..Arg::REQUIRED
};
static CMD: Command = Command {
name: "ex",
args: &[&OPTIONAL, &REQUIRED],
allow_missing_positional: true,
..Command::EMPTY
};
let one = argv(["value"]);
assert_eq!(
parse(&CMD, &one).unwrap(),
vec![Event::Arg {
arg: &REQUIRED,
value: b"value",
delimit: true
}]
);
let two = argv(["optional", "required"]);
assert_eq!(
parse(&CMD, &two).unwrap(),
vec![
Event::Arg {
arg: &OPTIONAL,
value: b"optional",
delimit: true
},
Event::Arg {
arg: &REQUIRED,
value: b"required",
delimit: true
},
]
);
}
#[test]
fn negative_numbers_are_narrowly_opted_in() {
static PLAIN: Flag = Flag {
key: 90,
name: "plain",
longs: &["plain"],
..Flag::VALUE
};
static VALUE: Arg = Arg {
key: 91,
name: "value",
..Arg::REQUIRED
};
static CMD: Command = Command {
name: "ex",
flags: &[&PLAIN],
args: &[&VALUE],
unknown_flags: Some(UnknownFlags::Error),
..Command::EMPTY
};
let flag = argv(["--plain", "-1"]);
assert_eq!(
parse(&CMD, &flag),
Err(Error::MissingFlagValue { flag: &PLAIN })
);
let positional = argv(["-1"]);
assert_eq!(
parse(&CMD, &positional),
Err(Error::UnknownFlag { token: b"-1" })
);
}
#[test]
fn an_exact_declared_digit_short_outranks_a_negative_number() {
static PRINT0: Flag = Flag {
key: 92,
name: "print0",
shorts: b"0",
..Flag::BOOL
};
static VALUE: Arg = Arg {
key: 93,
name: "value",
required: false,
allow_negative_numbers: true,
..Arg::REQUIRED
};
static CMD: Command = Command {
name: "fd",
flags: &[&PRINT0],
args: &[&VALUE],
unknown_flags: Some(UnknownFlags::Error),
..Command::EMPTY
};
assert_eq!(
parse(&CMD, &argv(["-0"])),
Ok(vec![Event::Flag {
flag: &PRINT0,
value: None,
negated: false,
}])
);
assert!(matches!(
parse(&CMD, &argv(["-1"])),
Ok(events) if matches!(events.as_slice(), [Event::Arg { value: b"-1", .. }])
));
}
#[test]
fn negation_of_value_flag_does_not_consume_a_value() {
static MODE: Flag = Flag {
key: 9,
name: "mode",
longs: &["mode"],
negate: Some("no-mode"),
..Flag::VALUE
};
static NEGATED_VALUE: Command = Command {
name: "ex",
flags: &[&MODE],
args: &[&FILE],
..Command::EMPTY
};
let a = argv(["--no-mode", "input"]);
assert_eq!(
parse(&NEGATED_VALUE, &a).unwrap(),
vec![
Event::Flag {
flag: &MODE,
value: None,
negated: true
},
Event::Arg {
arg: &FILE,
value: b"input",
delimit: true,
}
]
);
}
#[test]
fn no_abbreviation() {
let a = argv(["--forc"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Arg {
arg: &FILE,
value: b"--forc",
delimit: true,
}]
);
assert!(matches!(
parse(&STRICT, &a),
Err(Error::UnknownFlag { token: b"--forc" })
));
}
#[test]
fn an_unknown_flag_is_a_value_by_default() {
let a = argv(["--wat", "keep"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Arg {
arg: &FILE,
value: b"--wat",
delimit: true,
},
Event::Arg {
arg: &REST,
value: b"keep",
delimit: true,
},
]
);
static ONE: Command = Command {
name: "ex",
args: &[&FILE],
..Command::EMPTY
};
let a = argv(["a", "--wat"]);
assert_eq!(
parse(&ONE, &a),
Err(Error::UnexpectedArg { token: b"--wat" })
);
}
#[test]
fn negation() {
let a = argv(["--no-color"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Flag {
flag: &COLOR,
value: None,
negated: true
}]
);
}
#[test]
fn short_bundle_and_attached_value() {
let a = argv(["-fj8"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Flag {
flag: &FORCE,
value: None,
negated: false
},
Event::Flag {
flag: &JOBS,
value: Some(b"8"),
negated: false
},
]
);
}
#[test]
fn short_value_strips_one_equals() {
for (tokens, want) in [(["-j=8"], &b"8"[..]), (["-j==8"], &b"=8"[..])] {
let a = argv(tokens);
let Event::Flag { value, .. } = parse(&ROOT, &a).unwrap()[0] else {
panic!("expected a flag");
};
assert_eq!(value, Some(want), "{tokens:?}");
}
}
#[test]
fn bare_dash_is_a_value() {
let a = argv(["-"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Arg {
arg: &FILE,
value: b"-",
delimit: true,
}]
);
}
#[test]
fn positionals_then_variadic() {
let a = argv(["one", "two", "three"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Arg {
arg: &FILE,
value: b"one",
delimit: true,
},
Event::Arg {
arg: &REST,
value: b"two",
delimit: true,
},
Event::Arg {
arg: &REST,
value: b"three",
delimit: true,
},
]
);
}
#[test]
fn subcommand_and_alias_route_the_same() {
for token in ["install", "i"] {
let a = argv([token]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Command(&INSTALL)],
"{token}"
);
}
}
#[test]
fn a_parent_argument_can_exclude_a_later_subcommand() {
let a = argv(["--force", "install"]);
assert!(matches!(
parse(&ARGUMENT_CONFLICT, &a),
Err(Error::SubcommandConflict { subcommand }) if subcommand.name == "install"
));
}
#[test]
fn subcommand_only_routes_before_a_positional_is_filled() {
let a = argv(["other", "install"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Arg {
arg: &FILE,
value: b"other",
delimit: true,
},
Event::Arg {
arg: &REST,
value: b"install",
delimit: true,
},
]
);
}
#[test]
fn a_word_naming_no_subcommand_goes_to_the_default_one() {
let a = argv(["build"]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![
Event::Command(&RUN),
Event::Arg {
arg: &RUN_TASK,
value: b"build",
delimit: true,
},
]
);
}
static SHARED_QUIET: Flag = Flag {
key: 300,
name: "quiet",
longs: &["quiet"],
..Flag::BOOL
};
static SHARED_FLAGS: &[&Flag] = &[&SHARED_QUIET];
static SHARED_WHAT: Arg = Arg {
key: 301,
name: "what",
..Arg::REQUIRED
};
static SHARED_ARGS: &[&Arg] = &[&SHARED_WHAT];
#[test]
fn concatenating_tables_keeps_the_order_they_were_given_in() {
const ARGS: &[&[&Arg]] = &[&[&FILE], SHARED_ARGS, &[&REST]];
static TABLE: [&Arg; table_len(ARGS)] = concat_args(ARGS);
assert_eq!(
TABLE.iter().map(|a| a.name).collect::<Vec<_>>(),
["file", "what", "rest"]
);
const WITH_GAPS: &[&[&Flag]] = &[&[], &[&FORCE], &[], SHARED_FLAGS, &[]];
static FLAGS: [&Flag; table_len(WITH_GAPS)] = concat_flags(WITH_GAPS);
assert_eq!(
FLAGS.iter().map(|f| f.longs).collect::<Vec<_>>(),
[&["force"], &["quiet"]]
);
}
#[test]
fn a_concatenated_table_parses_like_a_declared_one() {
const FLAG_GROUPS: &[&[&Flag]] = &[&[&FORCE], SHARED_FLAGS];
const ARG_GROUPS: &[&[&Arg]] = &[SHARED_ARGS, &[&REST]];
static FLAGS: [&Flag; table_len(FLAG_GROUPS)] = concat_flags(FLAG_GROUPS);
static ARGS: [&Arg; table_len(ARG_GROUPS)] = concat_args(ARG_GROUPS);
static JOINED: Command = Command {
name: "joined",
flags: &FLAGS,
args: &ARGS,
..Command::EMPTY
};
let a = argv(["--quiet", "one", "two", "--force"]);
assert_eq!(
parse(&JOINED, &a).unwrap(),
vec![
Event::Flag {
flag: &SHARED_QUIET,
value: None,
negated: false
},
Event::Arg {
arg: &SHARED_WHAT,
value: b"one",
delimit: true,
},
Event::Arg {
arg: &REST,
value: b"two",
delimit: true,
},
Event::Flag {
flag: &FORCE,
value: None,
negated: false
},
]
);
}
#[test]
fn an_unknown_flag_is_not_routed() {
for token in ["--wat", "-x"] {
let a = argv([token]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![Event::Arg {
arg: &TASK,
value: token.as_bytes(),
delimit: true,
}],
"{token} should bind where it was typed, not in the default subcommand"
);
}
}
#[test]
fn a_named_subcommand_is_not_routed() {
let a = argv(["install"]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![Event::Command(&INSTALL)]
);
}
#[test]
fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
static CATCH: Command = Command {
name: "ex",
flags: &[&VERBOSE],
subcommands: &[&INSTALL],
external_subcommand: true,
unknown_flags: Some(UnknownFlags::Error),
..Command::EMPTY
};
let a = argv(["foo", "--help", "bar"]);
assert_eq!(
parse(&CATCH, &a).unwrap(),
vec![Event::External { values: &a[..] }]
);
let a = argv(["install"]);
assert_eq!(parse(&CATCH, &a).unwrap(), vec![Event::Command(&INSTALL)]);
let a = argv(["--verbose", "foo", "--verbose"]);
assert_eq!(
parse(&CATCH, &a).unwrap(),
vec![
Event::Flag {
flag: &VERBOSE,
value: None,
negated: false
},
Event::External { values: &a[1..] }
]
);
let a = argv(["--wat"]);
assert_eq!(
parse(&CATCH, &a),
Err(Error::UnknownFlag { token: b"--wat" })
);
let a = argv(["-1", "rest"]);
assert_eq!(
parse(&CATCH, &a).unwrap(),
vec![Event::External { values: &a[..] }]
);
}
#[test]
fn a_default_subcommand_outranks_an_external_one() {
static CATCH_DEFAULT: Command = Command {
name: "ex",
subcommands: &[&RUN],
default_subcommand: Some(&RUN),
external_subcommand: true,
..Command::EMPTY
};
let a = argv(["build"]);
assert_eq!(
parse(&CATCH_DEFAULT, &a).unwrap(),
vec![
Event::Command(&RUN),
Event::Arg {
arg: &RUN_TASK,
value: b"build",
delimit: true,
}
]
);
}
#[test]
fn a_default_subcommand_starts_at_the_word_it_receives() {
let a = argv(["build"]);
let mut parser = Parser::new(&DEFAULTING, &a);
assert_eq!(parser.next_event(), Some(Ok(Event::Command(&RUN))));
assert_eq!(parser.command_start(), 0);
assert_eq!(
parser.next_event(),
Some(Ok(Event::Arg {
arg: &RUN_TASK,
value: b"build",
delimit: true,
}))
);
}
#[test]
fn the_default_can_be_named_by_an_alias() {
static BY_ALIAS: Command = Command {
name: "mise",
args: &[&TASK],
subcommands: &[&INSTALL],
default_subcommand: Some(find_subcommand(&[&INSTALL], "i")),
..Command::EMPTY
};
assert!(::core::ptr::eq(
BY_ALIAS.default_subcommand.expect("declared"),
&INSTALL
));
}
#[test]
fn a_name_outranks_another_commands_alias() {
static ALPHA: Command = Command {
name: "alpha",
aliases: &["run"],
key: 300,
..Command::EMPTY
};
static PLAIN_RUN: Command = Command {
name: "run",
key: 301,
..Command::EMPTY
};
for subcommands in [&[&ALPHA, &PLAIN_RUN] as &[&Command], &[&PLAIN_RUN, &ALPHA]] {
assert!(::core::ptr::eq(
find_subcommand(subcommands, "run"),
&PLAIN_RUN
));
let root: Command = Command {
name: "ex",
subcommands,
..Command::EMPTY
};
let a = argv(["run"]);
assert_eq!(parse(&root, &a).unwrap(), vec![Event::Command(&PLAIN_RUN)]);
let a = argv(["alpha"]);
assert_eq!(parse(&root, &a).unwrap(), vec![Event::Command(&ALPHA)]);
let a = argv(["help", "run"]);
match parse(&root, &a) {
Err(Error::Help { cmd, .. }) => {
assert!(
::core::ptr::eq(cmd, &PLAIN_RUN),
"got help for {}",
cmd.name
)
}
other => panic!("expected a help request, got {other:?}"),
}
}
}
#[test]
#[should_panic(expected = "two subcommands answer to the same name")]
fn an_alias_cannot_shadow_a_sibling_command() {
static ADD: Command = Command {
name: "add",
aliases: &["install"],
..Command::EMPTY
};
assert_unique_subcommand_names(&[&INSTALL, &ADD]);
}
#[test]
fn the_word_is_re_examined_against_the_command_it_reached() {
let a = argv(["lint"]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![Event::Command(&RUN), Event::Command(&LINT)]
);
}
#[test]
fn the_default_is_taken_at_most_once_per_parse() {
let a = argv(["lint", "zzz"]);
assert_eq!(
parse(&DEFAULTING, &a),
Err(Error::UnexpectedArg { token: b"zzz" }),
"the second word must not reach `deep`"
);
let a = argv(["lint", "deep", "zzz"]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![
Event::Command(&RUN),
Event::Command(&LINT),
Event::Command(&DEEP),
Event::Arg {
arg: &RUN_TASK,
value: b"zzz",
delimit: true,
},
]
);
}
#[test]
fn a_flag_before_the_word_still_belongs_to_the_root() {
let a = argv(["--verbose", "build"]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![
Event::Flag {
flag: &VERBOSE,
value: None,
negated: false
},
Event::Command(&RUN),
Event::Arg {
arg: &RUN_TASK,
value: b"build",
delimit: true,
},
]
);
}
#[test]
fn nothing_routes_after_the_separator() {
let a = argv(["--", "build"]);
assert_eq!(
parse(&DEFAULTING, &a).unwrap(),
vec![Event::Arg {
arg: &TASK,
value: b"build",
delimit: true,
}]
);
}
#[test]
fn globals_are_inherited_but_plain_flags_are_not() {
let a = argv(["install", "--verbose"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Command(&INSTALL),
Event::Flag {
flag: &VERBOSE,
value: None,
negated: false
}
]
);
let a = argv(["install", "--jobs", "8"]);
assert!(matches!(parse(&STRICT, &a), Err(Error::UnknownFlag { .. })));
assert!(matches!(
parse(&ROOT, &a),
Err(Error::UnexpectedArg { token: b"--jobs" })
));
}
#[test]
fn double_dash_protects_flaglike_values() {
let a = argv(["--", "--force", "-x"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Arg {
arg: &FILE,
value: b"--force",
delimit: true,
},
Event::Arg {
arg: &REST,
value: b"-x",
delimit: true,
},
]
);
}
#[test]
fn second_double_dash_is_a_value() {
let a = argv(["--", "a", "--", "b"]);
let values: Vec<&[u8]> = parse(&ROOT, &a)
.unwrap()
.iter()
.filter_map(|e| match e {
Event::Arg { value, .. } => Some(*value),
_ => None,
})
.collect();
assert_eq!(values, vec![&b"a"[..], &b"--"[..], &b"b"[..]]);
}
#[test]
fn allow_hyphen_values_takes_a_flaglike_detached_value() {
static ARGS: Flag = Flag {
key: 6,
name: "args",
longs: &["args"],
shorts: b"a",
takes_value: true,
allow_hyphen_values: true,
..Flag::BOOL
};
static DIR: Flag = Flag {
key: 7,
name: "working-dir",
longs: &["working-dir"],
shorts: b"d",
..Flag::VALUE
};
static HYPHEN: Command = Command {
name: "ex",
flags: &[&ARGS, &DIR],
args: &[&REST],
..Command::EMPTY
};
let a = argv(["-a", "-destroy"]);
assert_eq!(
parse(&HYPHEN, &a).unwrap(),
vec![Event::Flag {
flag: &ARGS,
value: Some(b"-destroy"),
negated: false
}]
);
let a = argv(["--args", "--", "-x"]);
assert_eq!(
parse(&HYPHEN, &a).unwrap(),
vec![
Event::Flag {
flag: &ARGS,
value: Some(b"--"),
negated: false
},
Event::Arg {
arg: &REST,
value: b"-x",
delimit: true,
},
]
);
}
#[test]
fn require_equals_refuses_a_detached_value() {
static INSPECT: Flag = Flag {
key: 8,
name: "inspect",
longs: &["inspect"],
shorts: b"i",
takes_value: true,
require_equals: true,
..Flag::BOOL
};
static EQ: Command = Command {
name: "ex",
flags: &[&INSPECT],
..Command::EMPTY
};
let a = argv(["--inspect=9229"]);
assert_eq!(
parse(&EQ, &a).unwrap(),
vec![Event::Flag {
flag: &INSPECT,
value: Some(b"9229"),
negated: false
}]
);
let a = argv(["--inspect", "9229"]);
assert!(matches!(
parse(&EQ, &a),
Err(Error::MissingFlagValue { .. })
));
let a = argv(["-i9229"]);
assert_eq!(
parse(&EQ, &a).unwrap(),
vec![Event::Flag {
flag: &INSPECT,
value: Some(b"9229"),
negated: false
}]
);
static ALL: Flag = Flag {
key: 9,
name: "all",
longs: &["all"],
shorts: b"a",
..Flag::BOOL
};
static BUNDLE: Command = Command {
name: "ex",
flags: &[&ALL, &INSPECT],
..Command::EMPTY
};
let a = argv(["-ai", "9229"]);
assert!(
matches!(parse(&BUNDLE, &a), Err(Error::MissingFlagValue { .. })),
"a require_equals short reached through a bundle still refuses the following word"
);
}
#[test]
fn default_missing_binds_when_the_value_is_left_off() {
static COLOR: Flag = Flag {
key: 9,
name: "color",
longs: &["color"],
takes_value: true,
default_missing: Some(b"always"),
..Flag::BOOL
};
static VERBOSE: Flag = Flag {
key: 10,
name: "verbose",
longs: &["verbose"],
..Flag::BOOL
};
static MISSING: Command = Command {
name: "ex",
flags: &[&COLOR, &VERBOSE],
..Command::EMPTY
};
let a = argv(["--color"]);
assert_eq!(
parse(&MISSING, &a).unwrap(),
vec![Event::Flag {
flag: &COLOR,
value: Some(b"always"),
negated: false
}]
);
let a = argv(["--color=never"]);
assert_eq!(
parse(&MISSING, &a).unwrap(),
vec![Event::Flag {
flag: &COLOR,
value: Some(b"never"),
negated: false
}]
);
let a = argv(["--color", "--verbose"]);
assert_eq!(
parse(&MISSING, &a).unwrap(),
vec![
Event::Flag {
flag: &COLOR,
value: Some(b"always"),
negated: false
},
Event::Flag {
flag: &VERBOSE,
value: None,
negated: false
},
]
);
let a = argv(["--color="]);
assert_eq!(
parse(&MISSING, &a).unwrap(),
vec![Event::Flag {
flag: &COLOR,
value: Some(b""),
negated: false
}]
);
}
#[test]
fn optional_flag_value_distinguishes_bare_and_explicit_forms() {
static BUMP: Flag = Flag {
key: 11,
name: "bump",
longs: &["bump"],
takes_value: true,
value_optional: true,
..Flag::BOOL
};
static OPTIONAL: Command = Command {
name: "ex",
flags: &[&BUMP],
..Command::EMPTY
};
assert_eq!(parse(&OPTIONAL, &argv([])).unwrap(), vec![]);
assert_eq!(
parse(&OPTIONAL, &argv(["--bump"])).unwrap(),
vec![Event::Flag {
flag: &BUMP,
value: None,
negated: false,
}]
);
assert_eq!(
parse(&OPTIONAL, &argv(["--bump=5"])).unwrap(),
vec![Event::Flag {
flag: &BUMP,
value: Some(b"5"),
negated: false,
}]
);
static INCLUDE: Flag = Flag {
key: 12,
name: "include",
longs: &["include"],
takes_value: true,
variadic: true,
value_optional: true,
..Flag::BOOL
};
static VERBOSE: Flag = Flag {
key: 13,
name: "verbose",
longs: &["verbose"],
..Flag::BOOL
};
static VARIADIC: Command = Command {
name: "ex",
flags: &[&INCLUDE, &VERBOSE],
args: &[&REST],
..Command::EMPTY
};
assert_eq!(
parse(&VARIADIC, &argv(["--include", "--verbose", "file"])).unwrap(),
vec![
Event::Flag {
flag: &INCLUDE,
value: None,
negated: false,
},
Event::Flag {
flag: &VERBOSE,
value: None,
negated: false,
},
Event::Arg {
arg: &REST,
value: b"file",
delimit: true,
},
]
);
}
#[test]
fn default_missing_with_require_equals_leaves_the_following_word() {
static INSPECT: Flag = Flag {
key: 11,
name: "inspect",
longs: &["inspect"],
takes_value: true,
require_equals: true,
default_missing: Some(b"9229"),
..Flag::BOOL
};
static BOTH: Command = Command {
name: "ex",
flags: &[&INSPECT],
args: &[&REST],
..Command::EMPTY
};
let a = argv(["--inspect"]);
assert_eq!(
parse(&BOTH, &a).unwrap(),
vec![Event::Flag {
flag: &INSPECT,
value: Some(b"9229"),
negated: false
}]
);
let a = argv(["--inspect", "80"]);
assert_eq!(
parse(&BOTH, &a).unwrap(),
vec![
Event::Flag {
flag: &INSPECT,
value: Some(b"9229"),
negated: false
},
Event::Arg {
arg: &REST,
value: b"80",
delimit: true,
},
]
);
let a = argv(["--inspect="]);
assert_eq!(
parse(&BOTH, &a).unwrap(),
vec![Event::Flag {
flag: &INSPECT,
value: Some(b""),
negated: false
}]
);
}
#[test]
fn variadic_flag_collects_until_a_flaglike_token() {
static INCLUDE: Flag = Flag {
key: 5,
name: "include",
longs: &["include"],
shorts: b"i",
takes_value: true,
variadic: true,
..Flag::BOOL
};
static GREEDY: Command = Command {
name: "ex",
flags: &[&INCLUDE, &FORCE],
args: &[&FILE],
..Command::EMPTY
};
let a = argv(["--include", "x", "y", "--force"]);
assert_eq!(
parse(&GREEDY, &a).unwrap(),
vec![
Event::Flag {
flag: &INCLUDE,
value: Some(b"x"),
negated: false
},
Event::Flag {
flag: &INCLUDE,
value: Some(b"y"),
negated: false
},
Event::Flag {
flag: &FORCE,
value: None,
negated: false
},
]
);
}
#[test]
fn value_terminators_end_variadic_owners_without_binding() {
static INCLUDE: Flag = Flag {
key: 92,
name: "include",
longs: &["include"],
takes_value: true,
variadic: true,
value_terminator: Some(b";"),
..Flag::BOOL
};
static ITEMS: Arg = Arg {
key: 93,
name: "items",
var: true,
value_terminator: Some(b";"),
..Arg::REQUIRED
};
static AFTER: Arg = Arg {
key: 94,
name: "after",
..Arg::REQUIRED
};
static FLAG_CMD: Command = Command {
name: "ex",
flags: &[&INCLUDE],
args: &[&AFTER],
..Command::EMPTY
};
static ARG_CMD: Command = Command {
name: "ex",
args: &[&ITEMS, &AFTER],
..Command::EMPTY
};
let flag = argv(["--include", "a", ";", "tail"]);
assert_eq!(
parse(&FLAG_CMD, &flag).unwrap(),
vec![
Event::Flag {
flag: &INCLUDE,
value: Some(b"a"),
negated: false,
},
Event::Arg {
arg: &AFTER,
value: b"tail",
delimit: true,
},
]
);
let positional = argv(["a", ";", "tail"]);
assert_eq!(
parse(&ARG_CMD, &positional).unwrap(),
vec![
Event::Arg {
arg: &ITEMS,
value: b"a",
delimit: true,
},
Event::Arg {
arg: &AFTER,
value: b"tail",
delimit: true,
},
]
);
}
#[test]
fn a_non_variadic_flag_leaves_the_next_word_alone() {
let a = argv(["--jobs", "8", "keep-me"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![
Event::Flag {
flag: &JOBS,
value: Some(b"8"),
negated: false
},
Event::Arg {
arg: &FILE,
value: b"keep-me",
delimit: true,
},
]
);
}
#[test]
fn double_dash_seen_means_a_separator_was_typed() {
static FILES: Arg = Arg {
key: 23,
name: "files",
double_dash: DoubleDash::Automatic,
..Arg::VAR
};
static AUTO: Command = Command {
name: "ex",
flags: &[&FORCE],
args: &[&FILES],
..Command::EMPTY
};
let a = argv(["--", "x"]);
let mut parser = Parser::new(&ROOT, &a);
while parser.next_event().is_some() {}
assert!(parser.double_dash_seen(), "a real separator was consumed");
let a = argv(["x", "--force"]);
let mut parser = Parser::new(&AUTO, &a);
while parser.next_event().is_some() {}
assert!(
!parser.double_dash_seen(),
"automatic mode must not claim a separator was given"
);
}
#[test]
fn a_wrapper_still_forwards_a_help_flag() {
static ARGS: Arg = Arg {
key: 24,
name: "args",
..Arg::VAR
};
static WRAP: Command = Command {
name: "wrap",
args: &[&ARGS],
..Command::EMPTY
};
let a = argv(["--", "--help", "-h"]);
assert_eq!(
parse(&WRAP, &a).unwrap(),
vec![
Event::Arg {
arg: &ARGS,
value: b"--help",
delimit: true,
},
Event::Arg {
arg: &ARGS,
value: b"-h",
delimit: true,
},
]
);
static AUTO_ARGS: Arg = Arg {
key: 25,
name: "args",
double_dash: DoubleDash::Automatic,
..Arg::VAR
};
static AUTO_WRAP: Command = Command {
name: "wrap",
args: &[&AUTO_ARGS],
..Command::EMPTY
};
let a = argv(["node", "--help"]);
assert_eq!(
parse(&AUTO_WRAP, &a).unwrap(),
vec![
Event::Arg {
arg: &AUTO_ARGS,
value: b"node",
delimit: true,
},
Event::Arg {
arg: &AUTO_ARGS,
value: b"--help",
delimit: true,
},
]
);
let a = argv(["--help"]);
assert_eq!(
parse(&AUTO_WRAP, &a).unwrap(),
vec![Event::Flag {
flag: &HELP_LONG,
value: None,
negated: false
}]
);
}
#[test]
fn double_dash_required_arg() {
static CMD: Arg = Arg {
key: 20,
name: "cmd",
double_dash: DoubleDash::Required,
..Arg::REQUIRED
};
static EXEC: Command = Command {
name: "ex",
args: &[&CMD],
..Command::EMPTY
};
let a = argv(["--", "ls"]);
assert_eq!(
parse(&EXEC, &a).unwrap(),
vec![Event::Arg {
arg: &CMD,
value: b"ls",
delimit: true,
}]
);
let a = argv(["ls"]);
assert_eq!(
parse(&EXEC, &a),
Err(Error::ArgRequiresDoubleDash { arg: &CMD })
);
}
#[test]
fn double_dash_preserve_keeps_the_separator() {
static ARGS: Arg = Arg {
key: 21,
name: "args",
double_dash: DoubleDash::Preserve,
..Arg::VAR
};
static WRAP: Command = Command {
name: "ex",
args: &[&ARGS],
..Command::EMPTY
};
let a = argv(["a", "--", "b"]);
let values: Vec<&[u8]> = parse(&WRAP, &a)
.unwrap()
.iter()
.filter_map(|e| match e {
Event::Arg { value, .. } => Some(*value),
_ => None,
})
.collect();
assert_eq!(values, vec![&b"a"[..], &b"--"[..], &b"b"[..]]);
}
#[test]
fn double_dash_automatic_stops_flag_interpretation() {
static FILES: Arg = Arg {
key: 22,
name: "files",
double_dash: DoubleDash::Automatic,
..Arg::VAR
};
static AUTO: Command = Command {
name: "ex",
flags: &[&FORCE],
args: &[&FILES],
..Command::EMPTY
};
let a = argv(["-f", "one", "--force"]);
assert_eq!(
parse(&AUTO, &a).unwrap(),
vec![
Event::Flag {
flag: &FORCE,
value: None,
negated: false
},
Event::Arg {
arg: &FILES,
value: b"one",
delimit: true,
},
Event::Arg {
arg: &FILES,
value: b"--force",
delimit: true,
},
]
);
}
#[test]
fn too_many_words() {
static ONE: Command = Command {
name: "ex",
args: &[&FILE],
..Command::EMPTY
};
let a = argv(["a", "b"]);
assert_eq!(parse(&ONE, &a), Err(Error::UnexpectedArg { token: b"b" }));
}
#[test]
fn unknown_letter_rejects_the_whole_bundle() {
let a = argv(["-fz"]);
let mut parser = Parser::new(&STRICT, &a);
assert_eq!(
parser.next_event(),
Some(Err(Error::UnknownFlag { token: b"-fz" })),
"an unknown letter must reject the token before any of it is applied"
);
assert!(parser.next_event().is_none());
let a = argv(["-fz"]);
assert_eq!(
parse(&ROOT, &a).unwrap(),
vec![Event::Arg {
arg: &FILE,
value: b"-fz",
delimit: true,
}]
);
}
#[test]
fn unknown_short_error_names_the_whole_token() {
for (tokens, want) in [(["-z"], &b"-z"[..]), (["-fz"], &b"-fz"[..])] {
let a = argv(tokens);
assert_eq!(
parse(&STRICT, &a),
Err(Error::UnknownFlag { token: want }),
"{tokens:?}"
);
}
}
#[test]
fn errors_are_terminal() {
let a = argv(["--wat", "--force"]);
let mut parser = Parser::new(&STRICT, &a);
assert!(parser.next_event().unwrap().is_err());
assert!(parser.next_event().is_none());
}
#[test]
fn non_utf8_values_still_parse() {
let raw = OsStr::new("--force");
let a = [raw];
assert!(parse(&ROOT, &a).is_ok());
assert!(as_str(b"ok").is_ok());
assert!(as_str(&[0xff, 0xfe]).is_err());
}
#[test]
fn a_multicall_applet_is_the_basename_unless_it_is_the_dispatcher() {
assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
assert_eq!(
multicall_applet("/usr/bin/ls", "busybox", Some("busybox")),
Some("ls")
);
assert_eq!(
multicall_applet("/usr/bin/busybox", "busybox", Some("busybox")),
None
);
assert_eq!(
multicall_applet("ls.exe", "busybox", Some("busybox")),
Some("ls")
);
assert_eq!(
multicall_applet("/usr/bin/busybox", "BusyBox", Some("/opt/bin/busybox")),
None
);
assert_eq!(
multicall_applet("busybox.exe", "BusyBox", Some("busybox.exe")),
None
);
}
#[test]
fn a_spec_request_is_the_first_word_and_nothing_else() {
let request = [OsStr::new(SPEC_REQUEST)];
assert!(is_spec_request(&ROOT, &request));
let later = ["install", SPEC_REQUEST].map(OsStr::new);
assert!(!is_spec_request(&ROOT, &later));
assert!(!is_spec_request(&ROOT, &[]));
assert!(!is_spec_request(&ROOT, &[OsStr::new("--help")]));
}
#[test]
fn a_declared_command_of_that_name_keeps_it() {
static DECLARED: Command = Command {
name: SPEC_REQUEST,
key: 200,
..Command::EMPTY
};
static ALIASED: Command = Command {
name: "describe",
aliases: &[SPEC_REQUEST],
key: 201,
..Command::EMPTY
};
static DECLARES_IT: Command = Command {
name: "ex",
subcommands: &[&DECLARED],
..Command::EMPTY
};
static ALIASES_IT: Command = Command {
name: "ex",
subcommands: &[&ALIASED],
..Command::EMPTY
};
let request = [OsStr::new(SPEC_REQUEST)];
assert!(!is_spec_request(&DECLARES_IT, &request));
assert!(!is_spec_request(&ALIASES_IT, &request));
}
}