rust-args-parser 1.0.1

Tiny, fast, callback-based CLI argument parser for Rust
Documentation
# rust_args_parser

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

- 📦 Crate: `rust-args-parser`
- 📚 Docs: <https://docs.rs/rust-args-parser>
- 🔧 MSRV: **1.60**
- ⚖️ License: **MIT OR Apache-2.0**
- 📝 [Changelog]CHANGELOG.md

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

```rust
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) -> ap::Result<()> { c.verbose = c.verbose.saturating_add(1); Ok(()) }
fn set_json(c: &mut Ctx) -> ap::Result<()> { c.json = true; Ok(()) }
fn set_jobs(v: &OsStr, c: &mut Ctx) -> ap::Result<()> {
    let n: u32 = v.to_string_lossy().parse().map_err(|_| ap::Error::User("invalid --jobs".into()))?;
    c.jobs = Some(n); Ok(())
}
fn set_input(v: &OsStr, c: &mut Ctx) -> ap::Result<()> { c.input = Some(v.to_string_lossy().into()); Ok(()) }

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("0.1.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"))
        .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 `-`.

---

## Subcommands

Subcommands are nested `CmdSpec`s and **scoped**.

```rust
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) -> ap::Result<()> { c.remote = Some(v.to_string_lossy().into()); Ok(()) }
fn set_branch(v: &OsStr, c: &mut Ctx) -> ap::Result<()> { c.branch = Some(v.to_string_lossy().into()); Ok(()) }
fn push_file(v: &OsStr, c: &mut Ctx) -> ap::Result<()> { c.files.push(v.to_string_lossy().into()); Ok(()) }

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")`, `.repeat(Repeat::Many)`, `.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.

```rust
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.

```rust
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:

```rust
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
- `subcommands.rs` — nested commands, leaf scoping
- `env_defaults.rs` — ENV/default precedence
- `git.rs` — realistic multi-command layout

Run:

```bash
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.

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

---

## License

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