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 add
ing 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 typeC
(ex:Vec<T>
orOption<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-valueParameter
(applies to bothParameter::argument
&Parameter::option
). This is the most common field to use in your Cli.Collection
: defines a multi-valueParameter
(applies to bothParameter::argument
&Parameter::option
). This field allows you to configure the cardinality (aka:Nargs
) for any collection that implements Collectable.blarg
provides thisCollectable
implementations forVec<T>
andHashSet<T>
.Switch
: defines a no-valueParameter::option
(not applicable toParameter::argument
). This is used when specifying Cli flags (ex:--verbose
). Note thatSwitch
may apply to any typeT
(not restricted to justbool
).Optional
: defines aParameter::option
(not applicable toParameter::argument
). This field is used exclusively to specify anOption<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 branch
ing 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 branch
ed, 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 matcha b
into a cardinality=2 argument, andc
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 matchx
andy
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 matcha b c
into the first greedy argument, andd 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
- 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
- A collection that accepts a precise number of values:
- 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. - 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 intostruct MyStruct { a: u32, b: u32 }
.
§Features
unit_test
: For features that help with unit testing. SeeSubCommand
.tracing_debug
: Enables debug ofblarg
itself viatracing
.
Modules§
- derive
- Derive Api for
blarg
configuration. - prelude
- Traits which, typically, may be imported without concern:
use blarg::prelude::*
.
Structs§
- Collection
- A parameter that takes multiple values (specifiable
Nargs
). - Command
Line Parser - The base command line parser.
- Condition
- The condition argument with which to branch the parser.
Used with
CommandLineParser::branch
. - General
Parser - The configured command line parser.
Built via
CommandLineParser::build
orSubCommandParser::build
. - Optional
- An option parameter that maps down to
Option
, taking a single value (precisely 1). - Parameter
- An argument/option for the command parser.
Used with
CommandLineParser::add
andSubCommand::add
. - Scalar
- An option parameter that takes a single value (precisely 1).
- SubCommand
- A sub-command line parser.
- SubCommand
Parser - The sub-command parser.
- Switch
- An option parameter that takes no values (precisely 0).
Enums§
- Nargs
- The cardinality of inputs to match for an argument/option.
Traits§
- CliArgument
- Marker trait for capturable types that can formulate an argument in the Cli.
- CliOption
- Marker trait for capturable types that can formulate an option in the Cli