macro_rules! argue {
(
$( #[doc = $enum_doc:expr] )*
$enum_vis:vis $enum:ident,
$( #[doc = $iter_doc:expr] )*
$iter_vis:vis $iter:ident,
$( $key:ident $( $key_lit:literal )+, )*
$( @options $( $keyvalue:ident $( $keyvalue_lit:literal )+, )+ )?
$( @catchall-paths $path:ident, )?
@catchall $other:ident $otheros:ident,
) => { ... };
(
$( #[doc = $enum_doc:expr] )*
$enum_vis:vis $enum:ident,
$( #[doc = $iter_doc:expr] )*
$iter_vis:vis $iter:ident,
$( $key:ident $( $key_lit:literal )+, )*
$( @options $( $keyvalue:ident $( $keyvalue_lit:literal )+, )+ )?
$( @catchall-paths $path:ident, )?
) => { ... };
(
$( $key:ident $( $key_lit:literal )+, )*
$( @options $( $keyvalue:ident $( $keyvalue_lit:literal )+, )+ )?
$( @catchall-paths $path:ident, )?
$( @catchall $other:ident $otheros:ident, )?
) => { ... };
}Expand description
§Generate a CLI Argument Enum and Parser/Iterator.
This macro generates a custom enum and iterator to help with CLI argument parsing.
argue! is intended for use cases requiring more than the standard library’s
barebones args_os helper, but less than the
full-service offerings (and overhead) of a crate like clap.
It’ll automatically convert UTF-8 arguments to Strings (without
panicking), untangle combined key/value pair representations like -kval
or --key=val, and stop if/when it encounters an end-of-command terminator
("--").
The subsequent validation and handling, however, are left entirely up to you. Loop, match, and proceed however you see fit!
§Example
use argyle::argue;
// Construct the enum and iterator.
argue! {
// By default, this macro will call the enum "Argument" and the
// iterator "ArgumentIter". If you'd rather they be called something
// else, or have a non-private scope, you can override the defaults
// by kicking things off with the following.
/// # My Arguments Enum.
///
/// If you supply documentation like this
#[doc = "and/or like this"]
/// it'll be attached to the generated object.
pub // You can optionally change the scope like so.
MyArgument, // A name and trailing comma are required.
MyArgumentIter, // Naked works too if you don't care about docs/scope,
// though clippy may scold you. ;)
// --------------------
// If you have valueless keywords, they come next as a comma-separated
// list.
//
// Each entry needs an ident for the variant name and one or more
// string literals to match against.
Help "-h" "--help",
Version "-V" "--version",
Stderr "--stderr",
// --------------------
// If you have option keywords, those come next, but require an
// "@options" marker to announce their presence.
@options
// The list format is otherwise identical to their valueless
// counterparts.
Format "--format",
Level "-l" "--level",
// --------------------
// If you'd like to differentiate unmatched _paths_ from arbitrary
// string values, you can declare a variant for the purpose like so.
@catchall-paths Path,
// --------------------
// Last but not least, the enum will need two catchall variants to
// handle unmatched String and OsString values.
//
// By default, these are auto-generated as "Other" and "OtherOs",
// but if you'd like to call them something else, now's the time!
@catchall Invalid InvalidUtf8,
}
/// # Main.
fn main() {
// Example settings.
let mut stderr = false;
let mut format: Option<Format> = None;
let mut level = 0_u8;
let mut paths: Vec<PathBuf> = Vec::new();
// Loop through the environmental arguments, taking whatever actions
// make sense for your application.
for arg in MyArgument::args_os() {
match arg {
// You named these!
MyArgument::Help => print_help(),
MyArgument::Version => print_version(),
MyArgument::Stderr => { stderr = true; },
// Options come with the value as a String.
MyArgument::Format(v) => {
format = Format::from_str(v);
},
MyArgument::Level(v) => {
level = v.parse().unwrap_or(0);
},
// If you specified @catchall-paths, unmatched OsString values
// that happen to be (valid) filesystem paths will be mapped
// thusly (instead of to a generic catchall).
MyArgument::Path(v) => {
paths.push(PathBuf::from(v));
},
// Unmatched String values map to the first generic catchall.
MyArgument::Invalid(v) => {
eprintln!("Warning: unrecognized CLI argument {v}.");
},
// Unmatched values with invalid UTF-8 will be passed through
// to the second generic catchall as OsString values.
MyArgument::InvalidUtf8(v) => {
eprintln!(
"Warning: unrecognized CLI argument {}.",
v.display(),
);
},
}
}
// Now that the settings have been worked out, do something!
// …
}§Generated Code.
If you’re curious or need to do something more complicated, taking a look at the generated code can be helpful.
The call to argue! in the previous example, for example, will have added
the following to the module:
#[derive(Debug, Clone, Eq, PartialEq)]
/// # My Arguments Enum.
///
/// If you supply documentation like this and/or like this it'll be
/// attached to the generated object.
pub enum MyArgument {
/// # Matches "-h" "--help".
Help,
/// # Matches "-V" "--version".
Version,
/// # Matches "--stderr".
Stderr,
/// # Matches "--format".
Format(String),
/// # Matches "-l" "--level".
Level(String),
/// # Unassociated Path Value.
Path(OsString),
/// # Unspecified Value.
Invalid(String),
/// # Unspecified Value (Invalid UTF-8).
InvalidUtf8(OsString),
}
impl MyArgument {
/// # Environmental Argument Iterator.
///
/// Return a new [`MyArgumentIter`] instance seeded with [`ArgsOs`]
/// (minus the first entry corresponding to the executable path).
pub fn args_os() -> MyArgumentIter<Skip<ArgsOs>> {
// …
}
}
#[derive(Debug, Clone)]
struct MyArgumentIter<T> {
// …
}
// Note: the generated member methods share the parent's scope. The
// iterator was left private in the example, so the generated methods are
// private too.
impl<T: Iterator<Item=OsString>> MyArgumentIter<T> {
#[inline]
#[must_use]
/// # New Instance.
///
/// Create and return a new parsing iterator over any arbitrary
/// iterator of `OsString`.
const fn new(src: T) -> Self {
// …
}
#[inline]
#[must_use]
/// # Into Inner (Iterator).
///
/// Return what's left of the inner iterator.
fn into_inner(self) -> T {
// …
}
}
impl<T: Iterator<Item=OsString>> Iterator for MyArgumentIter<T> {
type Item = MyArgument;
fn next(&mut self) -> Option<Self::Item> {
// …
}
}
impl<T: Iterator<Item=OsString>> FusedIterator for MyArgumentIter<T> {}§Keyword Formatting
The macro supports (practically) any number of named keywords, with or without values, but there are rules for the literals they match against to ensure proper parsing.
- Short keys —
"-k"— must be exactly two bytes: a hyphen and an ASCII alphanumeric. - Long keys —
"--key"— must start with two hyphens and an ASCII alphanumeric, and contain only alphanumerics, hyphens, and underscores thereafter. - Commands —
"keyword"— must start with an ASCII alphanumeric, and contain only alphanumerics, hyphens, and underscores thereafter.
Format sanity is evaluated at compile-time, so issues like the following will trigger an error.
argyle::argue! {
MyArgument,
MyArgumentIter,
Level "-level", // Not short enough.
}argyle::argue! {
MyArgument,
MyArgumentIter,
Level "-❤️", // Cute, but not ASCII alphanumeric.
}argyle::argue! {
MyArgument,
MyArgumentIter,
FooBar "--foo bar", // Whitespace is illegal.
}argyle::argue! {
MyArgument,
MyArgumentIter,
Build "build!!!", // Settle down…
}This probably goes without saying, but keyword idents and literals must also be unique. Haha.
§Parsing Particulars
Key/value pairs are parsed identically whether they appear consecutively
— e.g. --key then value — or combined in any of the following ways:
-kvalue-k=value-k = value--key=value--key = value
Option values must, however, be valid UTF-8, otherwise the key and value
will be returned as a joint OtherOs(OsString) in --key=value format.
Keyword matches are otherwise a case-sensitive, all-or-nothing affair.
Parsing will stop early if an end-of-command terminator ("--") is
encountered. If your program needs to handle what comes after, adjust
the loop like so:
// Save the iterator to a variable and traverse it one value at a time
// to keep it in scope.
let mut args = MyArgument::args_os();
while let Some(arg) = args.next() {
// Process as normal.
}
// Create a second iterator instance from the remains of the first to
// loop through whatever was left, if anything.
for arg in MyArgumentIter::new(args.into_inner()) {
// Do something.
}