Thanks to visit codestin.com
Credit goes to lib.rs

11 releases (3 stable)

2.0.0 Feb 14, 2026
1.0.1 Dec 27, 2025
1.0.0 Nov 15, 2025
0.4.3 Nov 12, 2025
0.1.1 Sep 30, 2025

#191 in Command-line interface

33 downloads per month
Used in wali

MIT/Apache

74KB
1.5K SLoC

rust_args_parser

Tiny, fast, callback-based CLI argument parser for Rust.

This crate is a pragmatic alternative to heavyweight frameworks when you want:

  • Callbacks: options/positionals map directly to functions that mutate your context.
  • Subcommands (nested CmdSpec) with aliases.
  • Short clusters (-vvj8) and long forms (--jobs=8).
  • Numeric look-ahead so tokens like -1, -.5, +3.14, 1e3 are treated as values, not options.
  • Groups: mutually exclusive (Xor) / at least one required (ReqOne).
  • ENV/Default overlays with clear precedence (CLI > ENV > Default).
  • Readable matches with scope and provenance.

Quick start

use rust_args_parser as ap;
use std::ffi::OsStr;

#[derive(Default, Debug)]
struct Ctx {
    verbose: u8,
    json: bool,
    jobs: Option<u32>,
    input: Option<String>,
}

fn inc_verbose(c: &mut Ctx) { c.verbose = c.verbose.saturating_add(1); }
fn set_json(c: &mut Ctx) { c.json = true; }

fn jobs_is_u32(v: &OsStr) -> Result<(), &'static str> {
    v.to_string_lossy().parse::<u32>().map(|_| ()).map_err(|_| "invalid --jobs")
}

fn set_jobs(v: &OsStr, c: &mut Ctx) {
    // Safe because `jobs_is_u32` validates first.
    c.jobs = Some(v.to_string_lossy().parse::<u32>().unwrap());
}

fn set_input(v: &OsStr, c: &mut Ctx) { c.input = Some(v.to_string_lossy().into()); }

fn main() -> ap::Result<()> {
    // Global environment for parsing (and help rendering, if enabled)
    let env = ap::Env { wrap_cols: 80, color: ap::ColorMode::Auto, suggest: true, auto_help: true, version: Some("2.0.0"), author: None };

    // Command spec
    let spec = ap::CmdSpec::new("demo")
        .help("Demo tool")
        .opt(ap::OptSpec::flag("verbose", inc_verbose).short('v').long("verbose").help("Enable verbose output"))
        .opt(ap::OptSpec::flag("json", set_json).long("json").help("JSON output"))
        .opt(ap::OptSpec::value("jobs", set_jobs).short('j').long("jobs").metavar("N").help("Worker threads").validator(jobs_is_u32))
        .pos(ap::PosSpec::new("INPUT", set_input).range(0, 1));

    let mut ctx = Ctx::default();
    let argv: Vec<_> = std::env::args_os().skip(1).collect();

    match ap::parse(&env, &spec, &argv, &mut ctx) {
        Err(ap::Error::ExitMsg { code, message }) => {
            if let Some(m) = message { println!("{}", m); }
            std::process::exit(code);
        }
        Err(e) => { eprintln!("error: {e}"); std::process::exit(2); }
        Ok(m) => {
            println!("ctx   = {:?}", ctx);            // callbacks applied
            println!("leaf  = {:?}", m.leaf_path());  // selected command path
            Ok(())
        }
    }
}

CLI behavior

  • Short clusters: -vvj8-v -v -j 8 (flag callback fires once per -v).
  • Inline/next-arg values: -j8 / -j 8, --jobs=8 / --jobs 8.
  • Negative numbers: -d-3, --delta -3 are values (not options).
  • End-of-options: -- makes the rest positional, even if they start with -.

Migration to 2.0.0

Version 2.0.0 separates parse-level errors (unknown options, missing values, etc.) from user callback failures.

What changed

  • Infallible by default: CmdSpec::handler, CmdSpec::validator, OptSpec::{flag,value}, PosSpec::new callbacks now return ().
  • Fallible variants: use *_try (handler_try, validator_try, flag_try, value_try, new_try) when your callback needs to fail with a user-defined error:
    • Result<(), E> where E: std::error::Error + Send + Sync + 'static
  • Validators:
    • validator(...) accepts Result<(), E> where E: std::fmt::Display and maps failures to Error::User(String).
    • validator_try(...) accepts Result<(), E> where E: std::error::Error + Send + Sync + 'static and maps failures to Error::UserAny(...).
  • parse(...) still returns Result<Matches, rust_args_parser::Error>. Parse errors are unchanged; user failures surface as Error::User(...) / Error::UserAny(...).

Before / after

1.x (callbacks returned ap::Result<()>)

use rust_args_parser as ap;
use std::ffi::OsStr;

fn set_jobs(v: &OsStr, ctx: &mut Ctx) -> ap::Result<()> {
    ctx.jobs = Some(v.to_string_lossy().parse::<u32>().map_err(|_| ap::Error::User("invalid --jobs".into()))?);
    Ok(())
}

2.0.0 (infallible by default)

use rust_args_parser as ap;
use std::ffi::OsStr;

fn jobs_is_u32(v: &OsStr) -> Result<(), &'static str> {
    v.to_string_lossy().parse::<u32>().map(|_| ()).map_err(|_| "invalid --jobs")
}

fn set_jobs(v: &OsStr, ctx: &mut Ctx) {
    // Safe because `jobs_is_u32` validates first.
    ctx.jobs = Some(v.to_string_lossy().parse::<u32>().unwrap());
}

let spec = ap::CmdSpec::new("tool")
    .opt(ap::OptSpec::value("jobs", set_jobs).long("jobs").validator(jobs_is_u32));

2.0.0 (fallible user callback, typed error)

use rust_args_parser as ap;
use std::{error::Error, ffi::OsStr, fmt};

#[derive(Debug)]
struct AppError(&'static str);
impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.0) } }
impl Error for AppError {}

fn set_jobs_try(v: &OsStr, ctx: &mut Ctx) -> Result<(), AppError> {
    let n = v.to_string_lossy().parse::<u32>().map_err(|_| AppError("invalid --jobs"))?;
    ctx.jobs = Some(n);
    Ok(())
}

let spec = ap::CmdSpec::new("tool")
    .opt(ap::OptSpec::value_try("jobs", set_jobs_try).long("jobs"));

Subcommands

Subcommands are nested CmdSpecs and scoped.

use rust_args_parser as ap; use std::ffi::OsStr;
#[derive(Default)] struct Ctx { remote: Option<String>, branch: Option<String>, files: Vec<String> }
fn set_remote(v: &OsStr, c: &mut Ctx) { c.remote = Some(v.to_string_lossy().into()); }
fn set_branch(v: &OsStr, c: &mut Ctx) { c.branch = Some(v.to_string_lossy().into()); }
fn push_file(v: &OsStr, c: &mut Ctx) { c.files.push(v.to_string_lossy().into()); }

let spec = ap::CmdSpec::new("tool")
    .subcmd(
        ap::CmdSpec::new("repo")
            .alias("r")
            .subcmd(
                ap::CmdSpec::new("push")
                    .pos(ap::PosSpec::new("REMOTE", set_remote).required())
                    .pos(ap::PosSpec::new("BRANCH", set_branch).required())
                    .pos(ap::PosSpec::new("FILE", push_file).many())
            )
    );

let mut ctx = Ctx::default();
let m = ap::parse(&env, &spec, &argv, &mut ctx)?;
assert_eq!(m.leaf_path(), vec!["repo", "push"]);
let v = m.view();
assert_eq!(v.pos_one("BRANCH").unwrap(), OsStr::new("main"));

Root options are not accepted after you descend into a subcommand unless re-declared at that level.


Options, positionals, groups, validators

Options

  • Flag: OptSpec::flag("name", on_flag)
  • Value: OptSpec::value("name", on_value)
  • Builders: .short('j'), .long("jobs"), .metavar("N"), .help(""), .env("VAR"), .default(OsString), .group("name"), .repeatable(), .validator(fn)

Positionals

  • PosSpec::new("NAME", on_value) then choose one:
    • .required()
    • .many() (0..∞)
    • .range(min, max)
  • Also .help(""), .validator(fn).

Groups

  • GroupMode::Xor — options in the same group are mutually exclusive.
  • GroupMode::ReqOne — require at least one option from the group.
let spec = ap::CmdSpec::new("fmt")
    .opt(ap::OptSpec::flag("json", |_| Ok(())).long("json").group("fmt"))
    .opt(ap::OptSpec::flag("yaml", |_| Ok(())).long("yaml").group("fmt"))
    .group("fmt", ap::GroupMode::Xor);

Validators

Validators run on CLI, ENV, and Default values. If a validator fails, the callback for that option/positional is not invoked.


Overlays & provenance

  • Precedence: CLI > ENV > Default.
  • Bind ENV via .env("NAME"), defaults via .default().
  • Check where a value came from with matches.is_set_from(name, Source::{Cli,Env,Default}).
  • Matches is scoped: use m.view() for the leaf command or m.at(&[]) for root.

Built-ins & features

Feature flags (enabled by default unless you disable default-features):

  • help — built-in -h/--help and --version returning Error::ExitMsg { code: 0, message }.
  • color — colorized help output (honors NO_COLOR), with ColorMode::{Auto,Always,Never}.
  • suggest — suggestions for unknown options/commands.

Matches & views

Matches collects everything the parser saw. MatchView gives you a scoped, read-only accessor.

let m: ap::Matches = ap::parse(&env, &spec, &argv, &mut ctx)?;
let leaf = m.view();          // leaf scope
let root = m.at(&[]);         // root scope

leaf.is_set("verbose");
root.is_set_from("limit", ap::Source::Env);
leaf.value("jobs");          // first value
leaf.values("file");         // all values for an option
leaf.pos_one("INPUT");       // single positional by name
leaf.pos_all("FILE");        // all positionals with that name

Flags are stored as presence (Value::Flag). The parser also counts flag occurrences internally so -vvv calls the flag callback three times.


Errors

Top-level error type: ap::Error.

  • Error::User(String) / Error::UserAny(Box<dyn Error + Send + Sync>)
  • Error::Parse(String)
  • Error::ExitMsg { code, message }
  • Structured diagnostics:
    • UnknownOption { token, suggestions }
    • UnknownCommand { token, suggestions }
    • MissingValue { opt }
    • UnexpectedPositional { token }

Typical handling:

match ap::parse(&env, &spec, &argv, &mut ctx) {
    Err(ap::Error::ExitMsg { code, message }) => { if let Some(m) = message { println!("{}", m); } std::process::exit(code) }
    Err(e) => { eprintln!("error: {e}"); std::process::exit(2) }
    Ok(m) => { /* use ctx and/or m */ }
}

Utilities (ap::util)

  • looks_like_number_token(&str) -> bool-1, +3.14, -.5, 1e3, -1.2e-3.
  • strip_ansi_len(&str) -> usize — visible length, ignoring minimal ANSI sequences used in help.

Examples

See examples/:

  • basic.rs — flags, values, callbacks, errors
  • try_handlers.rs*_try callbacks with typed user errors
  • subcommands.rs — nested commands, leaf scoping
  • env_defaults.rs — ENV/default precedence
  • git.rs — realistic multi-command layout

Run:

cargo run --example basic -- --help

Testing

A comprehensive test suite covers options/positionals, subcommands, groups, overlays, validators, suggestions, help, utils, and an end-to-end golden test.

cargo test --features "help suggest color"
# or core only
cargo test

License

Dual-licensed under MIT or Apache-2.0 at your option.

No runtime deps