Macro argwerk::define

source ·
macro_rules! define {
    (
        $(#$attr:tt)*
        $vis:vis struct $name:ident { $($body:tt)* }
        $($config:tt)*
    ) => { ... };
}
Expand description

Parse command-line arguments.

This will generate an anonymous structure containing the arguments defined which is returned by the macro.

Each branch is executed when an incoming argument matches and must return a Result, like Ok(()). Error raised in the branch will cause a ErrorKind::Error error to be raised associated with that argument with the relevant error attached.

§The generated arguments structure

The first part of the macro defines the state available to the parser. These are field-like declarations which can specify a default initialization value. Fields which do not specify a value will be initialized using Default::default. This is the only required component of the macro.

The macro produces an arguments struct with fields matching this declaration. This can be used to conveniently group and access data populated during argument parsing.

You can use arbitrary attributes for the struct. Note that std::fmt::Debug will be automatically derived.

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    #[derive(Default)]
    struct Args {
        help: bool,
        limit: usize = 10,
    }
    /// Print this help.
    ["-h" | "--help"] => {
        help = true;
    }
    /// Specify a limit (default: 10).
    ["--limit", n] => {
        limit = str::parse(&n)?;
    }
}

let args = Args::parse(["--limit", "20"].iter().copied())?;

if args.help {
    println!("{}", Args::help());
}

assert_eq!(args.help, false);
assert_eq!(args.limit, 20);

This structure also has two associated functions which can be used to parse input:

  • args - Which parses OS arguments using std::env::args_os.
  • parse - Which can be provided with a custom iterator. This is what’s used in almost all the examples.

When using the custom parse function each item produced by the passed in iterator must implement TryIntoInput. This is implemented by types such as: &str, String, OsString and &OsStr.

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    struct Args {
        help: bool,
        limit: usize = 10,
        positional: Option<(String, String, String)>,
    }
    /// Print this help.
    ["-h" | "--help"] => {
        help = true;
    }
    [a, b, c] => {
        positional = Some((a, b, c));
    }
}

let args = Args::args()?;

if args.help {
    println!("{}", Args::help());
}

let args = Args::parse(vec!["foo", "bar", "baz"])?;

assert_eq!(args.positional, Some((String::from("foo"), String::from("bar"), String::from("baz"))));

§Parsing switches likes --help

The basic form of an argument branch one which matches on a string literal. The string literal (e.g. "--help") will then be treated as the switch for the branch. You can specify multiple matches for each branch by separating them with a pipe (|).

It’s not necessary that switches start with -, but this is assumed for convenience. In particular, argwerk will treat any arguments starting with a hyphen as “switch-like”. This is used to determine whether an argument is present if its optional (see later section).

argwerk::define! {
    #[usage = "command [-h]"]
    struct Args {
        help: bool
    }
    ["-h" | "--help"] => {
        help = true;
    }
}

let args = Args::parse(vec!["-h"])?;

if args.help {
    println!("{}", Args::help());
}

assert_eq!(args.help, true);

§Parsing positional arguments

Positional arguments are parsed by specifying a vector of bindings in a branch. Like [foo, bar].

The following is a basic example. Two arguments foo and bar are required if the branch matches. If there is no such input an ErrorKind::MissingPositional error will be raised.

argwerk::define! {
    #[usage = "command [-h]"]
    struct Args {
        positional: Option<(String, String)>,
    }
    [foo, bar] if positional.is_none() => {
        positional = Some((foo, bar));
    }
}

let args = Args::parse(["a", "b"].iter().copied())?;

assert_eq!(args.positional, Some((String::from("a"), String::from("b"))));

§Help documentation

You specify documentation for switches and arguments using doc comments (e.g. /// Hello World). These are automatically wrapped to 80 characters.

Documentation can be formatted with the help associated function, which returns a static instance of Help. This is also available as the HELP static variable inside of match branches. Help formatting can be further customized using Help::format.

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    struct Args {
        help2: bool,
    }
    /// Prints the help.
    ///
    /// This includes:
    ///    * All the available switches.
    ///    * All the available positional arguments.
    ///    * Whatever else the developer decided to put in here! We even support wrapping comments which are overly long.
    ["-h" | "--help"] => {
        println!("{}", HELP.format().width(120));
    }
    ["--help2"] => {
        help2 = true;
    }
}

let args = Args::args()?;

// Another way to access and format help documentation.
if args.help2 {
    println!("{}", Args::help().format().width(120));
}

Invoking this with -h would print:

Usage: command [-h]
A simple test command.

This is nice!

Options:
  -h, --help  Prints the help.

              This includes:
                 * All the available switches.
                 * All the available positional arguments.
                 * Whatever else the developer decided to put in here! We even
                   support wrapping comments which are overly long.

We determine the initial indentation level from the first doc comment. Looking at the code above, this would be the line containing Prints the help.. We then wrap additional lines relative to this level of indentation.

We also determine the individual indentation level of a line by looking at all the non-alphanumerical character that prefixes that line. That’s why the “overly long” markdown list bullet above wraps correctly. Instead of wrapping at the *, it wraps to the first alphanumeric character after it.

§Required arguments using #[required]

You can specify required arguments using the #[required] attribute in the field specification. Fields which are marked as #[required] have the type Option<T>. If the field is left as uninitialized (None) once all arguments have been parsed will cause an error to be raised. See ErrorKind::MissingRequired.

A reason that the argument is required can be optionally provided by doing #[required = "--name is required"].

§Examples

argwerk::define! {
    struct Args {
        #[required = "--name must be used"]
        name: String,
    }
    ["--name", n] => {
        name = Some(n);
    }
}

let args = Args::parse(vec!["--name", "John"])?;
assert_eq!(args.name, "John");

§Raw os arguments with #[os]

In argwerk you can specify that a branch takes a raw argument using the #[os] attribute. This value will then be an OsString and represents exactly what was fed to your program from the operating system.

use std::ffi::OsString;

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    struct Args {
        raw: Option<OsString>,
    }
    ["--raw", #[os] arg] => {
        raw = Some(arg);
    }
}

let args = Args::parse(vec![OsString::from("--raw"), OsString::from("baz")])?;

assert!(args.raw.is_some());

§Capture all available arguments using #[rest]

You can write a branch that receives all available arguments using the #[rest] attribute. This can be done both with arguments to switches, and positional arguments.

You can get the rest of the arguments in their raw form using #[rest(os)].

The following showcases capturing using a positional argument:

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    struct Args {
        rest: Vec<String>,
    }
    [#[rest] args] => {
        rest = args;
    }
}

let args = Args::parse(["foo", "bar", "baz"].iter().copied())?;

assert_eq!(args.rest, &["foo", "bar", "baz"]);

And the following through a switch:

argwerk::define! {
    #[usage = "command [-h]"]
    struct Args {
        rest: Vec<String>,
    }
    ["--test", #[rest] args] => {
        rest = args;
    }
}

let args = Args::parse(["--test", "foo", "bar", "baz"].iter().copied())?;

assert_eq!(args.rest, &["foo", "bar", "baz"]);

This showcases getting raw os arguments using #[rest(os)]:

use std::ffi::{OsString, OsStr};

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    struct Args {
        rest: Vec<OsString>,
    }
    [#[rest(os)] args] => {
        rest = args;
    }
}

let args = Args::parse(["foo", "bar", "baz"].iter().copied())?;

assert_eq!(args.rest, &[OsStr::new("foo"), OsStr::new("bar"), OsStr::new("baz")]);

§Parsing optional arguments with #[option]

Switches and positional arguments can be marked with the #[option] attribute. This will cause the argument to take a value of type Option<I::Item> where I represents the iterator that is being parsed.

You can get an optional argument in its raw form using #[option(os)].

An optional argument parses to None if:

  • There are no more arguments to parse.
  • The argument is “switch-like” (starts with -).
use std::ffi::{OsString, OsStr};

argwerk::define! {
    /// A simple test command.
    #[usage = "command [-h]"]
    struct Args {
        foo: Option<String>,
        bar: bool,
        baz: Option<OsString>,
    }
    /// A switch taking an optional argument.
    ["--foo", #[option] arg] => {
        foo = arg;
    }
    ["--bar"] => {
        bar = true;
    }
    /// A switch taking an optional raw argument.
    ["--baz", #[option(os)] arg] => {
        baz = arg;
    }
}

// Argument exists, but looks like a switch.
let args = Args::parse(["--foo", "--bar"].iter().copied())?;
assert_eq!(args.foo.as_deref(), None);
assert!(args.bar);

// Argument does not exist.
let args = Args::parse(["--foo"].iter().copied())?;
assert_eq!(args.foo.as_deref(), None);
assert!(!args.bar);

let args = Args::parse(["--foo", "bar"].iter().copied())?;
assert_eq!(args.foo.as_deref(), Some("bar"));
assert!(!args.bar);

let args = Args::parse(["--baz"].iter().copied())?;
assert_eq!(args.baz.as_deref(), None);
assert!(!args.bar);

let args = Args::parse(["--baz", "bar"].iter().copied())?;
assert_eq!(args.baz.as_deref(), Some(OsStr::new("bar")));
assert!(!args.bar);