argue

Macro argue 

Source
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.
}