Crate blarg

source ·
Expand description

blarg is a command line parser for Rust.

Although other crates provide command line parser functionality, we have found they prioritize different concerns than those we are interested in. It is very possible those crates can be configured to make our desired command line parser. We built blarg to create our desired style of command line parser “out of the box”. Specifically, blarg attempts to prioritize the following design concerns:

  • Type safe argument parsing: The user should not call any &str -> T conversion functions directly.
  • Domain sensitive argument parsing: The user should not validate/reject any domain invalid inputs (see footnotes #1 for examples). Instead, the command line parser should be configurable to prevent these.
  • Argument vs. option paradigm: The basic Api design for constructing a command line parser is via arguments and options. Briefly, arguments are required parameters specified positionally on the Cli. Options are optional parameters specified via --.. or -.. syntax.
  • Sub-command paradigm: The user may configure sub-commands which act to collect multiple related programs into a single Cli.
  • Detailed yet basic UX: The help and error output of the Cli should be very detailed, leaving no ambiguity in how to use the program. However, we do not aim to support rich display configurations, such as colour output, shell completions, etc.
  • Reasonable performance: The command line parser should be fast enough. To be clear, we are of the opinion that the cost of argument parsing is insignificant with respect to any non-trivial program. That said, blarg will still aim to minimize its memory & CPU footprint, within reason.

As it currently stands, we feel blarg fits a niche role in the rust ecosystem. We hope you do as well!

Usage

This page includes a few demos on using blarg. More examples are outlined in the source.

via derive Api:

use blarg::{derive::*, Collection, CommandLineParser, Nargs, Parameter};

#[derive(Default, BlargParser)]
#[blarg(program = "summer")]
struct Parameters {
    #[blarg(help = "The items to sum.")]
    item: Vec<u32>,
}

fn main() {
    let parameters = Parameters::blarg_parse();
    let sum: u32 = parameters.item.iter().sum();
    println!("Sum: {sum}");
}

or equivalently via builder Api (this page):

use blarg::{Collection, CommandLineParser, Nargs, Parameter};

fn main() {
    let mut items: Vec<u32> = Vec::default();

    let clp = CommandLineParser::new("summer");
    let parser = clp
        .add(
            Parameter::argument(Collection::new(&mut items, Nargs::AtLeastOne), "item")
                .help("The items to sum."),
        )
        .build();

    parser.parse();
    let sum: u32 = items.iter().sum();
    println!("Sum: {sum}");
}

Both of these generate the same Cli program (with minor help message differences):

$ summer -h
usage: summer [-h] ITEM [...]
positional arguments:
 ITEM [...]  The items to sum.
options:
 -h, --help  Show this help message and exit.

$ summer 1 2 3
Sum: 6

$ summer
Parse error during matching: not enough tokens provided to parameter 'ITEM'.

^

$ summer 1 blah
Parse error during capture: cannot convert 'blah' to u32.
1 blah
  ^

Derive Api

We highly recommend using the derive Api to configure your Cli program. The next section explains the structure and semantics of blarg using the builder Api, which applies to both builder and derive Apis.

Builder Api

Configure blarg by starting with a CommandLineParser and adding parameters. There are two classes of parameters: Parameter::argument and Parameter::option.

Each parameter takes a field which serves to specify the following aspects on the Cli:

  • The underlying type T of the parameter (ex: u32).
  • Whether T is wrapped in a container type C (ex: Vec<T> or Option<T>).
  • The cardinality of the parameter (ex: 0, 1, N, at least 1, etc).

All type T parsing in blarg is controlled by std::str::FromStr. blarg will parse any parameter type T, as long as it implements std::str::FromStr.

The other aspects of parameter configuration relate to additional Cli usage and optics:

  • Parameter naming, including the short name of an Parameter::option.
  • Description of the parameter when displaying --help.

Fields

  • Scalar: defines a single-value Parameter (applies to both Parameter::argument & Parameter::option). This is the most common field to use in your Cli.
  • Collection: defines a multi-value Parameter (applies to both Parameter::argument & Parameter::option). This field allows you to configure the cardinality (aka: Nargs) for any collection that implements Collectable. blarg provides this Collectable implementations for Vec<T> and HashSet<T>.
  • Switch: defines a no-value Parameter::option (not applicable to Parameter::argument). This is used when specifying Cli flags (ex: --verbose). Note that Switch may apply to any type T (not restricted to just bool).
  • Optional: defines a Parameter::option (not applicable to Parameter::argument). This field is used exclusively to specify an Option<T> type.

Sub-commands

To setup a sub-command based Cli, start with a root CommandLineParser. Both options and arguments may be added to the root parser via add. The sub-command section of the parser begins by branching this parser.

Branching takes a special Condition parameter which only allows a Scalar field. You may describe the sub-commands on the condition via Condition::choice. In blarg, any type T can be used to define sub-commands; sub-commands needn’t only be strings. See the Condition section below for further explanation.

Once branched, the result is a SubCommandParser that allows you to setup individual sub-commands. These are configured via SubCommandParser::command, which takes the variant of T to which the sub-command applies, and a impl FnOnce(SubCommand) -> SubCommand to setup the parser. From here, setup the sub-command via SubCommand::add.

Notice, the sub-command structure is dictated solely by the usage of command; usage of choice affects the display documentation only. As a side effect of this distinction, you may include “undocumented” sub-commands (as well as “false” sub-commands), both shown in the example below.

use blarg::{prelude::*, CommandLineParser, Condition, Parameter, Scalar, Switch};

fn main() {
    let mut sub: u32 = 0;
    let mut arg_0: bool = false;
    let mut opt_0: bool = false;
    let mut arg_1: bool = false;

    let clp = CommandLineParser::new("sub-command");
    let parser = clp
        .about("Describe the base command line parser.  Let's make it a little long for fun.")
        .branch(
            Condition::new(Scalar::new(&mut sub), "sub")
                // "0" is an undocumented sub-command.
                // "1" is a regular sub-command.
                .choice(1, "the one sub-command")
                // "2" is a regular sub-command.
                .choice(2, "the two sub-command")
                // "3" is a false sub-command.
                // It will appear in the documentation, but only those specified via `command(..)` actually affect the program structure.
                .choice(3, "the three sub-command"),
        )
        .command(0, |sub_command| {
            sub_command
                .about("Describe the 0 sub-command parser.  Let's make it a little long for fun.")
                .add(Parameter::argument(Scalar::new(&mut arg_0), "arg"))
                .add(Parameter::option(
                    Switch::new(&mut opt_0, true),
                    "opt",
                    None,
                ))
        })
        .command(1, |sub_command| {
            sub_command
                .about("Describe the 1 sub-command parser.")
                .add(Parameter::argument(Scalar::new(&mut arg_1), "arg"))
        })
        // Specify an argument-less & option-less sub-command by leaving the 'sub' untouched.
        .command(2, |sub_command| sub_command)
        // Since we never add "3", it isn't a true sub-command.
        .build();

    parser.parse();

    println!("Used sub-command '{sub}'.");
    match sub {
        0 => {
            println!("arg_0: {arg_0}");
            println!("opt_0: {opt_0}");
            assert!(!arg_1);
        }
        1 => {
            assert!(!arg_0);
            assert!(!opt_0);
            println!("arg_1: {arg_1}");
        }
        2 => {
            assert!(!arg_0);
            assert!(!opt_0);
            assert!(!arg_1);
            println!("argument-less & option-less");
        }
        _ => {
            panic!(
                "impossible - the parser will reject any variants not specified via `command(..)`."
            )
        }
    }
}
usage: sub-command [-h] SUB
positional arguments:
 SUB         {1, 2, 3}
   1           the one sub-command
   2           the two sub-command
   3           the three sub-command
<truncated>

$ sub-command 0 -h
usage: sub-command 0 [-h] ARG
positional arguments:
 ARG
options:
 -h, --help  Show this help message and exit.
 --opt

$ sub-command 0 true
Used sub-command '0'.
arg_0: true
opt_0: false

$ sub-command 0 false --opt
Used sub-command '0'.
arg_0: false
opt_0: true

$ sub-command 2
Used sub-command '2'.
argument-less & option-less

$ sub-command 3
Parse error during branching: unknown sub-command '3'.
3
^

Condition
In order to support arbitrary branching types T, we use an implicit (not compile-time enforced) requirement. Simply, std::str::FromStr for T must be inverted by std::fmt::Display for the same type T. In code, this means the following assertion must succeed.

let s: &str = "..";
let s_prime: String = T::from_str(s).unwrap().to_string();
assert_eq!(s_prime, s);

For more details on this requirement, see the Condition documentation.

Defaults & Initials

Technically, blarg has nothing to do with specifying default values for parameters. This may be confusing - defaults are a common feature for command line parsers! Instead, the defaults of your Cli will come from the variable initializations when configuring blarg. We support presenting initials over the help message via derive Api, but behaviourally blarg does not take part in setting parameter defaults.

// The default for the 'verbose' parameter is 'false'.
let mut verbose: bool = false;
// The default for the 'value' parameter is '0'.
let mut value: u32 = 0;

// Use `verbose` and `value` in the CommandLineParser.
// `GeneralParser::parse` will assign onto these variables.

We’d also like to point out: semantically, defaults only apply to options (Parameter::option). By definition, arguments (Parameter::argument) must be specified on the Cli, so having a “default” does not make sense.

In the case of Collection parameters (for both options and arguments), the initial value again comes from the variable initialization. As parameters are received from the Cli input, these are added to the collection (via Collectable). This may be unexpected if you think of setting a default value for the Collection, which is then reset upon receiving input.

When using blarg, we recommend thinking in terms of initial values that are later affected by the Cli invocation. In the case of non-Collection parameters, the initial value will be overwritten, but in the case of Collection parameters, the initial value will be extended.

// The initial for the 'items' parameter is '[0, 1, 2]'.
let mut items: Vec<u32> = vec![0, 1, 2];

// Use `items` in the CommandLineParser.
// `GeneralParser::parse` will `Collectable::add` to `items`.

Organization

It may be useful to organize your program variables into a single struct. Configuring such an organizational struct is made seamless with the derive Api. The following demonstrates how to manually configure an organizational struct with blarg. We also use this demo to illustrate testing your parser configuration.

use blarg::{Collection, CommandLineParser, GeneralParser, Nargs, Parameter, Switch};

#[derive(Debug, PartialEq, Eq)]
pub struct Params {
    verbose: bool,
    items: Vec<u32>,
}

impl Params {
    fn init() -> Self {
        Self {
            verbose: false,
            items: Vec::default(),
        }
    }
}

fn main() {
    let params = parse();
    let sum: u32 = params.items.iter().sum();
    println!("Sum: {sum}");
}

// Configure and execute the parser against `env::args`.
fn parse() -> Params {
    parse_tokens(|parser: GeneralParser| Ok(parser.parse()))
}

// Unit-testable function to configure the parser and execute it against the specified
fn parse_tokens(parse_fn: impl FnOnce(GeneralParser) -> Result<(), i32>) -> Params {
    let mut params = Params::init();

    let clp = CommandLineParser::new("organization");
    let parser = clp
        .add(Parameter::option(
            Switch::new(&mut params.verbose, true),
            "verbose",
            Some('v'),
        ))
        .add(Parameter::argument(
            Collection::new(&mut params.items, Nargs::AtLeastOne),
            "item",
        ))
        .build();

    // The parse_fn signature is a `Result`.
    // However, since `GeneralParser::parse` does not return an error (it uses `std::process::exit` under the hood), the `Err` case is only reached via test.
    parse_fn(parser).expect("test-reachable-only");
    params
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn parse_empty() {
        // Setup
        let tokens = vec![];

        // Execute & verify
        parse_tokens(|parser| parser.parse_tokens(tokens.as_slice()));
    }

    #[test]
    fn parse() {
        // Setup
        let tokens = vec!["5"];

        // Execute
        let result = parse_tokens(|parser| parser.parse_tokens(tokens.as_slice()));

        // Verify
        assert_eq!(
            result,
            Params {
                verbose: false,
                items: vec![5],
            }
        );
    }
}

Cli Semantics

blarg parses the Cli tokens according to the following set of rules. By and large this syntax should be familiar to many Cli developers, with a few subtle nuances for various edge cases.

  • Each parameter matches a number of tokens based off its cardinality.
  • Arguments are matched based off positional ordering. Once the expected cardinality is matched, then the parser naturally switches to the next parameter. For example, a b c will match a b into a cardinality=2 argument, and c into the next argument.
  • Options are matched based off the --NAME (or short name -N) specifier. Once specified, the cardinality is matched against the subsequent tokens. For example, --key x y will match x and y into a cardinality=2 option. Again, when the expected cardinality is matched, then the parser switches to the next parameter.
  • In both arguments and options, the Nargs * and + match greedily; they never switch over to the next parameter. This greedy matching can be broken by using an option as a separator (see footnotes #2 for guidance). For example, a b c --key value d e f will match a b c into the first greedy argument, and d e f into the second (assuming --key is a cardinality=1 option).
  • The key-value pair of a cardinality=1 option may be separated with the = character. Subsequent tokens always rollover to the next parameter, even if the option’s cardinality is greedy. For example, --key=123 is equivalent to --key 123. Also notice, only the first = character is used as a separator. For example, --key=123=456 is equivalent to --key 123=456 (see footnotes #3 for guidance).
  • The previous rule also applies to cardinality=1 options using the short name syntax. For example, -k=123 is equivalent to --key 123.
  • Multiple short named options may be combined into a single flag. For example, -abc is equivalent to --apple --banana --carrot. The = separator rule may be applied only to the final option in this syntax. For example, -abc=123 is equivalent to --apple --banana --carrot=123.

Field-Narg Interaction

Argument

Parameter         | Narg | Cardinality | Syntax           | Description
-----------------------------------------------------------------------------------------------
Scalar<T>         |      | [1]         | VALUE            | precisely 1
Collection<C<T>>  | n    | [n]         | VALUE .. VALUE   | precisely n
Collection<C<T>>  | *    | [0, ∞)      | [VALUE ...]      | any amount; captured greedily
Collection<C<T>>  | +    | [1, ∞)      | VALUE [...]      | at least 1; captured greedily

Option

Parameter         | Narg | Cardinality | Syntax                   | Description
-------------------------------------------------------------------------------------------------
Scalar<T>         |      | [1]         | [--NAME VALUE]           | precisely 1
Collection<C<T>>  | n    | [n]         | [--NAME VALUE .. VALUE]  | precisely n
Collection<C<T>>  | *    | [0, ∞)      | [--NAME [VALUE ...]]     | any amount; captured greedily
Collection<C<T>>  | +    | [1, ∞)      | [--NAME VALUE [...]]     | at least 1; captured greedily
Switch<T>         |      | [0]         | [--NAME]                 | precisely 0
Optional<T>       |      | [1]         | [--NAME VALUE]           | precisely 1

Footnotes

  1. Examples of domain sensitive argument parsing:
    • A collection that accepts a precise number of values: triple-input-program 1 2 3
    • A collection that de-duplicates values: set-input-program 1 2 1
  2. Although the greedy matching can be broken by an option, blarg does not recommend a Cli design that requires this tactic. Clis that use more than one * or + greedy parameter are complicated, and put a significant burden on the user to understand how to break the greedy matching.
  3. Using the equals sign inside a parameter can be a useful way to parse complex structs. In other words, you can write a custom std::str::FromStr deserializer. For example, a=123,b=456 could be deserialized into struct MyStruct { a: u32, b: u32 }.

Features

  • unit_test: For features that help with unit testing. See SubCommand.
  • tracing_debug: Enables debug of blarg itself via tracing.

Modules

  • Derive Api for blarg configuration.
  • Traits which, typically, may be imported without concern: use blarg::prelude::*.

Structs

Enums

  • The cardinality of inputs to match for an argument/option.

Traits

  • Marker trait for capturable types that can formulate an argument in the Cli.
  • Marker trait for capturable types that can formulate an option in the Cli