A Rust argument parser optimized for build time and binary size, whose parsers are .rodata not code.
Roargs is also unusually powerful in allowing flexible composition of sub-parsers and argument groups using algebraic data types.
Most derive-based parsers generate an imperative parse function per struct.
Unfortunately, this is a lot of work for the compiler and linker, and can produce large binaries on embedded.
roargs instead emits one small static descriptor per struct and ships a single shared engine that interprets them.
For our application, we have an embedded application that includes >100 distinct argument parsers, many of which share common arguments, so this difference really matters.
Measured against gumdrop on 124 small parser structs in one binary
(opt-level = "s", details in research/):
| roargs | gumdrop | |
|---|---|---|
| marginal size per parser struct | 495 B | 1,809 B |
| cold build, 30 parsers | 0.86 s | 2.92 s |
| dependencies (incl. derive) | 0 | syn 1.x |
Status This crate is being used internally, but is still under active development and the API may change.
[dependencies]
roargs = { git = "https://github.com/AutoPallet/roargs.git" }use roargs::Options;
/// Shared logging flags.
#[derive(Options)]
struct Logging {
/// print each step
verbose: bool,
/// silence everything else
quiet: bool,
}
/// Feed the cat.
#[derive(Options)]
struct Feed {
/// grams of kibble
#[options(default = "20.0")]
grams: f32,
#[options(flatten)]
logging: Logging,
// Vec<> arguments can be repeated
name: Vec<String>
}
fn main() {
// argv convenience wrapper: prints help/errors and exits.
let args: Feed = roargs::parse_args();
if args.logging.verbose {
println!("dispensing {} g", args.grams);
}
}use roargs::Options;
/// Creep silently closer.
#[derive(Options)]
struct Stalk {
/// distance to close, in tail-lengths
#[options(required)]
dist: f32,
}
/// Commit to the leap.
#[derive(Options)]
struct Pounce {
/// how high to arc
#[options(required)]
height: f32,
/// wiggle before takeoff
wiggle: bool,
}
#[derive(Options)]
enum Tactic {
Stalk(Stalk),
Pounce(Pounce),
}
/// Send the cat after the mouse.
#[derive(Options)]
struct Chase {
#[options(free, required)]
x: f32,
#[options(free, required)]
y: f32,
/// Rehearse without moving.
dry_run: bool,
tactic: Option<Tactic>, // scoped enum: `--tactic pounce --height 2.0`
}
fn main() {
let c: Chase = roargs::parse_args();
let how = match c.tactic {
Some(Tactic::Stalk(s)) => format!("stalking the last {} tail-lengths", s.dist),
Some(Tactic::Pounce(p)) if p.wiggle => format!("wiggle-pouncing {} high", p.height),
Some(Tactic::Pounce(p)) => format!("pouncing {} high", p.height),
None => "just ambling over".to_string(),
};
let dry = if c.dry_run { "[dry run] " } else { "" };
println!("{dry}cat charges to ({}, {}), {how}", c.x, c.y);
}Any parser can be embedded into another parser in one of several ways:
#[derive(Options)]
struct SubParser {
flag: bool
}
#[derive(Options)]
struct Cmd {
/// All flags and arguments are treated as part of this command at the top-level,
/// but grouped visually in help text.
#[options(flatten)]
flattened: SubParser,
/// The user must pass --nested followed by (optionally) flags from SubParser.
/// Passing any other flag from Cmd will exit the nested scope.
nested: SubParser,
/// Like other flags, subparsers can be optional
maybe: Option<SubParser>,
}
// Example usage: cmd --flag --nested --flag --maybederive(Options) on an enum produces a subcommand parser, which can be used positionally to approximate a normal subcommand or used directly as a normal or repeated argument.
#[derive(Options)]
enum Subcommand {
Status,
/// bare value payload: `commit "message"`
Commit(String),
}
#[derive(Options)]
struct Git {
verbose: bool,
#[options(free)]
cmd: Subcommand,
}| Attribute | Applies to | Effect |
|---|---|---|
free |
fields | positional: bare tokens fill values |
required |
fields | error if the field is never set |
long = "name" |
fields, enum variants | override the auto kebab-cased name |
short = "c" |
fields | explicit short flag (auto shorts: unique first letter, never h) |
help = "text" |
fields | help text (/// doc comments work too) |
default = "20.0" |
fields | value parsed via FromStr when the flag is absent |
default_expr = "…" |
fields | Rust expression evaluated as the default |
flatten |
struct fields | inline the sub-struct's fields, no opener token |
command |
enum fields | classic subcommand: the rest of the line belongs to it |
default |
unit enum variants | variant taken when nothing selects one (std #[default] is honored too) |
Feature notes that used to live here (scoped-nesting semantics, auto
short-flag rules, arg_value!, the no-print engine contract and host
helpers, VersionAbort) moved to docs/features.md.
Design notes: research/descriptor-format.md;
benchmark harness: research/size-bench/.
MIT or Apache-2.0, at your option.