a low-footprint, derive-first cli parser for rust.
the derive emits a flat &'static description of your command and one non-generic
engine interprets it, so you get familiar derive ergonomics, almost no compile-time
overhead, and nothing pulled in at runtime.
[dependencies]
pound = "0.1"field shape carries meaning, so most fields need no attribute:
| field type | meaning |
|---|---|
bool |
flag, present means true |
T |
required positional |
Option<T> |
optional positional |
Vec<T> |
repeatable / variadic |
#[pound(short)] and #[pound(long)] promote any of these to a named option.
use pound::Parse;
/// fetch urls to disk
#[derive(Parse)]
#[pound(name = "grab", version = "0.1.0")]
struct Grab {
url: Vec<String>, // variadic positional
/// write downloads here
#[pound(short, long)]
output: Option<String>, // -o / --output <path>
/// overwrite existing files
#[pound(short, long)]
force: bool, // -f / --force
/// increase verbosity, pass multiple times
#[pound(short, long, count)]
verbose: u8, // -v / -vvv / --verbose
/// parallel jobs
#[pound(short, long, default = "4")]
jobs: u32, // --jobs <n> (default: 4)
}
fn main() {
let grab = Grab::parse(); // exits and prints help on -h/--help or a parse error
println!("{grab:?}");
}derive Parse on an enum and each variant becomes a subcommand. struct variants
carry the subcommand's own flags and positionals:
use pound::Parse;
/// a small package manager
#[derive(Parse)]
#[pound(name = "pkg", version = "1.0.0")]
enum Pkg {
/// initialise a project
Init {
#[pound(short, long)]
force: bool,
},
/// add a dependency
Add {
name: String, // required positional
url: String, // required positional
#[pound(short, long)]
force: bool,
},
}
fn main() {
match Pkg::parse() {
Pkg::Init { force } => { /* ... */ }
Pkg::Add { name, url, force } => { /* ... */ }
}
}pkg init --force
pkg add serde https://crates.io/crates/serde -f
a struct can carry global flags and delegate the rest of the command line to a
subcommand enum via #[pound(subcommand)]:
#[derive(Parse)]
enum Action {
Build { #[pound(short, long)] release: bool },
Clean,
Test,
}
#[derive(Parse)]
#[pound(name = "tool", version = "0.1.0")]
struct Cli {
#[pound(short, long)]
verbose: bool,
#[pound(long, default = "info")]
log: String,
#[pound(subcommand)]
action: Action, // required, shows help if absent
}
fn main() {
let cli = Cli::parse();
}tool --verbose build --release
tool --log debug clean
make the subcommand optional with Option<T>:
#[derive(Parse)]
#[pound(name = "maybe")]
struct Cli {
#[pound(short, long)]
force: bool,
#[pound(subcommand)]
action: Option<Action>, // ok to omit entirely
}enum variants can themselves carry a #[pound(subcommand)] field, nesting as
deep as you need:
#[derive(Parse)]
enum LeaseAction { Open, Close }
#[derive(Parse)]
#[pound(name = "cade")]
enum Cade {
Lease {
#[pound(subcommand)]
action: LeaseAction, // cade lease open | cade lease close
},
Status,
}hidden subcommands
annotate a variant with #[pound(hidden)] to accept it without listing it in help:
#[derive(Parse)]
#[pound(name = "svc")]
enum Svc {
Run,
#[pound(hidden)]
Internal, // parses fine, invisible in --help
}#[derive(ValueEnum)] turns a unit enum into a FromArg type. variants are
accepted as kebab-case strings and the valid choices appear automatically in help
text and error messages:
use pound::{Parse, ValueEnum};
#[derive(ValueEnum)]
enum Level { Quiet, Normal, Trace }
#[derive(Parse)]
#[pound(name = "run")]
struct Run {
#[pound(long)]
level: Level, // --level quiet|normal|trace
}$ run --level bogus
error: invalid value 'bogus' for --level [possible values: quiet, normal, trace]
group = "name" puts flags into a named set. by default the group is optional
(at most one). add required_group = "name" at the item level to require exactly one:
#[derive(Parse)]
#[pound(name = "pick", required_group = "speed")]
struct Pick {
#[pound(long, group = "speed")] fast: bool,
#[pound(long, group = "speed")] slow: bool,
}pick --fast ✓
pick ✗ error: one of --fast / --slow is required
pick --fast --slow ✗ error: --fast conflicts with --slow
for a one-off conflict without a named group, use conflicts_with = "field":
#[derive(Parse)]
#[pound(name = "log")]
struct Log {
#[pound(long)]
quiet: bool,
#[pound(long, conflicts_with = "quiet")]
verbose: bool,
}#[pound(trailing)] collects everything after -- into a Vec<String>:
#[derive(Parse)]
#[pound(name = "sandbox")]
struct Sandbox {
#[pound(short, long)]
sockets: bool,
#[pound(trailing)]
exec: Vec<String>, // sandbox -- ls -la
}implement FromArg for any type you want to parse directly from the command line:
use pound::{FromArg, ValueError};
struct Rgb(u8, u8, u8);
impl FromArg for Rgb {
fn from_arg(s: &str) -> Result<Self, ValueError> {
let s = s.strip_prefix('#').unwrap_or(s);
if s.len() != 6 {
return Err(ValueError::new(s, "expected a 6-digit hex colour"));
}
let byte = |i: usize| u8::from_str_radix(&s[i..i + 2], 16)
.map_err(|e| ValueError::new(s, e));
Ok(Rgb(byte(0)?, byte(2)?, byte(4)?))
}
}| attribute | meaning |
|---|---|
name = "str" |
command name in help/usage (defaults to the type name, lowercased) |
version = "str" |
version shown by -V / --version |
required_group = "g" |
exactly one flag in group g must be provided |
| attribute | meaning |
|---|---|
short |
short flag (-f from field name, or = 'x' to override) |
long |
long flag (--field-name, or = "name" to override) |
positional |
force positional parsing (usually inferred from the type) |
trailing |
collect everything after -- into a Vec<String> |
count |
count repeated flags into a uN (-vvv → 3) |
default = "str" |
default value, parsed the same way as a user-supplied string |
env = "VAR" |
fall back to environment variable VAR (cli > env > default; std only) |
value_name = "str" |
placeholder shown in usage (<PATH> instead of <output>) |
help = "str" |
override the doc comment for this field's help line |
group = "name" |
add to a named mutually-exclusive group |
conflicts_with = "f" |
this flag may not appear alongside field f |
alias = "a,b" |
extra long names that also match, kept out of help |
hidden |
accept the flag/argument but omit it from help |
subcommand |
delegate remaining args to this field's Parse enum |
| attribute | meaning |
|---|---|
name = "str" |
override the subcommand name |
alias = "a,b" |
extra names that also select the command, kept out of help |
hidden |
accept the command but hide it from help |
you can hand-build a CommandSpec and impl Parse yourself, which is handy for
dynamic or programmatic command trees. see pound/tests/cli.rs for a full
worked example.
| feature | default | description |
|---|---|---|
std |
yes | the std-only conveniences: parse() / try_parse() (read argv), Error::exit(), and the PathBuf value impl. turn it off for #![no_std] against alloc (see below) |
derive |
yes | enables #[derive(Parse)] and #[derive(ValueEnum)] |
help |
yes | bakes doc-comment help text in and enables the formatter; without it, -h shows a bare usage line |
disable all three with default-features = false for the leanest possible binary.
pound is #![no_std] when you turn the std feature off. it still needs an
allocator (matched values, the help formatter, and error messages use alloc),
but nothing from std:
[dependencies]
pound = { version = "0.1", default-features = false, features = ["derive", "help"] }what you give up without std, and what to use instead:
| std convenience | no_std replacement |
|---|---|
Parse::parse() (reads argv) |
feed args to Parse::try_parse_from(args) yourself |
Error::exit() |
match on the returned Error and decide how to bail |
FromArg for PathBuf |
impl FromArg for your own path type |
try_parse_from takes an IntoIterator<Item = &str>, and the parser never
copies an argument into an owned String. matched values borrow straight from
the input; only the fields you declare as String allocate, when they're read
out. the input just has to outlive the call, which always holds, since Self
owns whatever it keeps.
pound can't portably fetch argv; that's an OS detail std normally handles.
but if your program owns its libc entry point, it can hand pound the
(argc, argv) it already holds via args_from_raw:
use core::ffi::{c_char, c_int};
#[unsafe(no_mangle)]
pub extern "C" fn main(argc: c_int, argv: *const *const c_char) -> c_int {
// SAFETY: argc/argv are the unmodified parameters libc passed `main`.
let args = unsafe { pound::args_from_raw(argc, argv) }.skip(1);
match MyCommand::try_parse_from(args) {
Ok(cmd) => { /* ... */ 0 }
Err(e) => { /* render e */ 2 }
}
}the yielded &strs borrow straight from argv, so parsing stays zero-copy.
the same CLI built both ways (root flags, three subcommands, a value enum, a
repeatable option, defaults), release profile opt-level = "s", lto = "fat",
stripped, on one machine.
| parser | stripped binary | over a no-parser baseline |
|---|---|---|
| pound | 345 KiB | +60 KiB |
| clap | 517 KiB | +232 KiB |
| parser | cold debug | cold release | incremental |
|---|---|---|---|
| pound | 2.1 s | 4.9 s | 0.13 s |
| clap | 4.1 s | 9.3 s | 0.22 s |
| parser | per parse (try_parse_from) |
|---|---|
| pound | ~52 ns |
| clap | ~8.4 µs |
the dev environment is a nix flake. nix develop gives the toolchain,
nix develop .#fmt formats the tree (nightly rustfmt + taplo) on entry.
EUPL-1.2