nanoargs 0.1.1

A minimal, zero-dependency argument parser for Rust CLI applications
Documentation

πŸ“Ž nanoargs

Crates.io Docs.rs Build Status Coverage Status License

A lightweight, zero-dependency argument parser for Rust.

Part of the nano crate family β€” minimal, zero-dependency building blocks for Rust.

Everything you'd expect from a CLI parser β€” flags, options, subcommands, help generation, env fallback, typed parsing β€” with zero dependencies.

Why nanoargs?

Choosing a CLI parser in Rust usually feels like a compromise:

  • clap is the gold standard, but it's a heavy lift. It pulls in 10+ transitive dependencies, deep customization and vast api reference sheets.
  • pico-args / lexopt are zero-dep, but they leave the hard work to you. You'll end up hand-coding your own --help strings, ENV fallbacks, and subcommand logic.
  • nanoargs is the middle ground. You get the professional features you actually use like subcommands, help generation, and env fallbacks, with zero dependencies.
Feature nanoargs clap bpaf pico-args lexopt
Dependencies (transitive) 0 ~12* 5** 0 0
Auto help text βœ“ βœ“ βœ“ βœ— βœ—
Version flag (--version) βœ“ βœ“ βœ“ βœ— βœ—
Env var fallback βœ“ βœ“ βœ“ βœ— βœ—
Multi-value options βœ“ βœ“ βœ“ βœ— βœ—
Subcommands βœ“ βœ“ βœ“ βœ—β€  βœ—β€ 
Combined short flags (-abc) βœ“ βœ“ βœ“ βœ“Β§ βœ“
Default values βœ“ βœ“ βœ“ βœ— βœ—
Required args βœ“ βœ“ βœ“ βœ— βœ—
Hidden args βœ“ βœ“ βœ“ β€” β€”
Colored help βœ“Β§ βœ“ βœ“Β§ βœ— βœ—
Derive macros βœ— βœ“ βœ“ βœ— βœ—
Shell completions βœ— βœ“ βœ“Β§ βœ— βœ—
Other advanced features βœ— βœ“ βœ“ βœ— βœ—

* clap with default features. With derive, ~17 total. ** bpaf combinatoric API has 0 deps. With derive, 5 total (bpaf_derive + syn tree). † No built-in support. Achievable manually by matching on positional tokens. Β§ Via opt-in cargo features.

Which one should I use?

  • clap / bpaf: Your CLI is complex and needs deep customization and advanced support.
  • pico-args / lexopt: You’re building something tiny where most features aren't a priority.
  • nanoargs: You want a clean, intuitive API that supports 90% of use cases without taking on any dependencies.

Quick Start (full demo)

cargo add nanoargs
use nanoargs::{ArgBuilder, Flag, Opt, Pos, ParseError};

fn main() {
    let parser = ArgBuilder::new()
        .name("myapp")
        .description("A sample CLI tool")
        .version("1.0.0")
        .flag(Flag::new("verbose").desc("Enable verbose output").short('v'))
        .option(Opt::new("output").placeholder("FILE").desc("Output file path").short('o'))
        .positional(Pos::new("input").desc("Input file").required())
        .build()
        .unwrap();

    match parser.parse_env() {
        Ok(result) => {
            println!("verbose: {}", result.get_flag("verbose"));
            println!("output:  {:?}", result.get_option("output"));
            println!("input:   {:?}", result.get_positionals());
        }
        Err(ParseError::HelpRequested(text)) => print!("{text}"),
        Err(ParseError::VersionRequested(text)) => println!("{text}"),
        Err(e) => eprintln!("error: {e}"),
    }
}

See Parsing and Results and Error Handling for more details.

Defining Arguments

Flags (example)

Boolean switches toggled by presence.

let parser = ArgBuilder::new()
    .flag(Flag::new("verbose").desc("Enable verbose output").short('v'))
    .flag(Flag::new("dry-run").desc("Simulate without side effects"))
    .build();
myapp --verbose --dry-run
myapp -v

Options (example)

Key-value arguments with fluent modifiers. Construct an Opt with Opt::new(), chain .placeholder(), .desc(), .short(), .required(), .default(), .env(), .multi(), or .hidden() as needed, then pass it to .option().

let parser = ArgBuilder::new()
    .option(Opt::new("format").placeholder("FMT").desc("Output format").short('f'))
    .option(Opt::new("output").placeholder("FILE").desc("Output file path").short('o').required())
    .option(Opt::new("jobs").placeholder("NUM").desc("Parallel jobs").short('j').default("4"))
    .option(Opt::new("include").placeholder("DIR").desc("Directories to include").short('i').multi())
    .build();
myapp --output result.txt --jobs 8 --include src --include tests
myapp -o=result.txt -j 8

Positionals (example)

Unnamed arguments collected in order. Chain .required() on the Pos builder to make a positional mandatory.

let parser = ArgBuilder::new()
    .positional(Pos::new("input").desc("Input file").required())
    .positional(Pos::new("extra").desc("Additional arguments"))
    .build();
myapp input.txt extra1 extra2

Environment Variable Fallback (example)

Options can fall back to environment variables when not provided on the command line. Chain .env() on the Opt builder. The resolution order is: CLI value β†’ env var β†’ default β†’ error (if required).

let parser = ArgBuilder::new()
    .option(Opt::new("log-level").placeholder("LEVEL").desc("Log level").short('l').env("MYAPP_LOG_LEVEL"))
    .option(Opt::new("output").placeholder("FILE").desc("Output file").short('o').env("MYAPP_OUTPUT").required())
    .option(Opt::new("format").placeholder("FMT").desc("Output format").short('f').env("MYAPP_FORMAT").default("text"))
    .build();
# CLI value takes priority
myapp --output result.txt

# Falls back to env var when CLI option is omitted
MYAPP_OUTPUT=from_env.txt myapp

# Falls back to default when both CLI and env var are absent
myapp --output result.txt   # format resolves to "text"

Help text automatically shows the associated env var:

Options:
  -l, --log-level <LEVEL>  Log level [env: MYAPP_LOG_LEVEL]
  -o, --output <FILE>      Output file (required) [env: MYAPP_OUTPUT]
  -f, --format <FMT>       Output format [default: text] [env: MYAPP_FORMAT]

Hidden Arguments

Flags and options can be marked as hidden β€” they parse normally but are excluded from --help output. Useful for internal, debug, or deprecated arguments.

let parser = ArgBuilder::new()
    .flag(Flag::new("debug").desc("Enable debug mode").short('d').hidden())
    .option(Opt::new("trace-id").placeholder("ID").desc("Internal trace ID").hidden())
    .flag(Flag::new("verbose").desc("Enable verbose output").short('v'))
    .build();
# Hidden arguments work on the command line
myapp --debug --trace-id=abc123 --verbose

# But --help only shows --verbose
myapp --help

The .hidden() modifier is available on both Flag and Opt, and can be called in any order relative to other modifiers.

Combined Short Flags (example)

Combine multiple short flags into a single token. The parser walks characters left-to-right against the registered schema.

let parser = ArgBuilder::new()
    .flag(Flag::new("all").desc("Show all").short('a'))
    .flag(Flag::new("brief").desc("Brief output").short('b'))
    .flag(Flag::new("color").desc("Enable color").short('c'))
    .option(Opt::new("width").placeholder("NUM").desc("Column width").short('w'))
    .build();
# Combined flags
myapp -abc              # sets all, brief, color

# Attached option value
myapp -w10              # sets width to "10"

# Flags + option in one token
myapp -abcw10           # sets all, brief, color + width="10"
myapp -abcw 10          # same β€” value from next token

When the parser encounters an option character during the walk, it claims all remaining characters as the value. If none remain, it consumes the next argument token.

Subcommands (example)

Git-style subcommands, each with their own flags, options, and positionals. Global flags are parsed before the subcommand token.

let build_parser = ArgBuilder::new()
    .name("build")
    .description("Compile the project")
    .flag(Flag::new("release").desc("Build in release mode").short('r'))
    .build();

let test_parser = ArgBuilder::new()
    .name("test")
    .description("Run the test suite")
    .flag(Flag::new("verbose").desc("Show detailed output").short('v'))
    .build();

let parser = ArgBuilder::new()
    .name("myapp")
    .description("A demo CLI")
    .flag(Flag::new("quiet").desc("Suppress output").short('q'))
    .subcommand("build", "Compile the project", build_parser)
    .subcommand("test", "Run the test suite", test_parser)
    .build();
myapp build --release
myapp -q test --verbose
myapp --help              # lists available subcommands
myapp build --help        # subcommand-specific help

Note: When subcommands are registered, the first bare (non-flag/option) token is always treated as the subcommand name. Parent-level positional arguments are not supported alongside subcommands β€” this matches git-style CLI conventions.

# Supported β€” global flags before the subcommand:
myapp -q build --release

# NOT supported β€” positionals before the subcommand:
myapp file.txt build    # "file.txt" is treated as an unknown subcommand

Version Flag (example)

Built-in --version / -V support. Set a version string on the builder and the parser handles the rest.

let parser = ArgBuilder::new()
    .name("myapp")
    .version(env!("CARGO_PKG_VERSION"))
    .flag(Flag::new("verbose").desc("Enable verbose output").short('v'))
    .build()
    .unwrap();
$ myapp --version
myapp 0.1.0

$ myapp -V
myapp 0.1.0

The -V short flag is reserved when a version is configured β€” the builder will reject any user-registered flag or option that uses 'V' as its short form. When no version is set, --version and -V are treated as unknown arguments, and 'V' is available for user flags.

When both --help and --version appear, whichever comes first wins. After --, both are treated as positionals.

Parsing and Results

Accessors

parse_env() reads from std::env::args() and returns a Result<ParseResult, ParseError>:

let result = parser.parse_env()?;

// Flags return bool
let verbose = result.get_flag("verbose");

// Options return Option<&str>
let output = result.get_option("output");

// Multi-value options return &[String]
let tags = result.get_option_values("tags");

// Positionals in order
let positionals = result.get_positionals();

// Subcommand access
if let Some(name) = result.subcommand() {
    let sub = result.subcommand_result().unwrap();
}

You can also pass your own args with parser.parse(args) β€” see Error Handling for the full match pattern.

Typed Parsing

Parse option values into any type implementing FromStr. Convenience helpers collapse the common three-way match into a single call:

// With a default fallback β€” returns the parsed value, or the default if absent/unparseable
let jobs: u32 = result.get_option_or_default("jobs", 4);

// With a lazy default β€” closure only runs if needed
let jobs: u32 = result.get_option_or("jobs", || num_cpus());

// Required with Result β€” use the ? operator
let jobs: u32 = result.get_option_required("jobs")?;

For fine-grained control over parse errors, the original accessor is still available:

match result.get_option_parsed::<u32>("jobs") {
    Some(Ok(n)) => println!("jobs: {}", n),
    Some(Err(e)) => eprintln!("invalid jobs value: {}", e),
    None => println!("jobs not set"),
}

Error Handling (example)

match parser.parse(args) {
    Ok(result) => { /* use result */ }
    Err(ParseError::HelpRequested(text)) => print!("{}", text),
    Err(ParseError::VersionRequested(text)) => println!("{}", text),
    Err(ParseError::MissingRequired(name)) => eprintln!("missing: {}", name),
    Err(ParseError::MissingValue(name)) => eprintln!("no value for: --{}", name),
    Err(ParseError::UnknownArgument(token)) => eprintln!("unknown: {}", token),
    Err(ParseError::NoSubcommand(msg)) => eprintln!("{}", msg),
    Err(ParseError::UnknownSubcommand(name)) => eprintln!("unknown subcommand: {}", name),
    Err(ParseError::DuplicateOption(name)) => eprintln!("duplicate: --{}", name),
    Err(ParseError::InvalidFormat(msg)) => eprintln!("bad format: {}", msg),
}

Help and Output

Help Text (example)

Auto-generated from your schema. Triggered by --help or -h.

$ myapp --help
Usage: myapp [OPTIONS] <input> [extra]

A sample CLI tool

Options:
  -v, --verbose          Enable verbose output
      --dry-run          Simulate without side effects
  -o, --output <FILE>    Output file path (required)
  -j, --jobs <NUM>       Parallel jobs [default: 4]
  -h, --help             Print help

Colored Help (opt-in)

Enable the color feature to get ANSI-colored help text and error messages via nanocolor:

[dependencies]
nanoargs = { version = "0.1", features = ["color"] }
cargo run --example help_text --features color -- --help

When enabled, section headers are bold yellow, flag/option names are green, placeholders are cyan, and metadata like [default: ...] is dim. Error messages get a bold red error: prefix. Color is automatically suppressed when NO_COLOR is set or output is not a TTY (handled by nanocolor). Without the feature, the crate remains zero-dependency and output is unchanged.

Double-Dash Separator

Everything after -- is treated as a positional, even if it looks like a flag or option.

myapp -- --not-a-flag -abc
# positionals: ["--not-a-flag", "-abc"]

Schema-Free Parsing for Quick Scripts

parse_loose() skips the schema entirely β€” useful for throwaway scripts where defining flags and options feels like overkill.

fn main() {
    let result = nanoargs::parse_loose().unwrap();
    let verbose = result.get_flag("verbose");
    let output = result.get_option("output");
    let positionals = result.get_positionals();
}

It uses a heuristic to guess whether --key is a flag or an option: if the next token doesn't start with -, it's consumed as the value.

When it works well: simple scripts with clear flag/option boundaries (--verbose --output file.txt).

When it doesn't: --output -v silently treats --output as a flag (not an option), because -v starts with -. If your CLI has options that could receive flag-like values, use ArgBuilder instead.

API Reference

See the full API docs on docs.rs.

Examples

Example Description Run
flags Boolean flags cargo run --example flags -- -v --dry-run
options Options with defaults and required cargo run --example options -- -o=out.txt -j 8
positionals Positional arguments cargo run --example positionals -- file.txt extra
short_flags Combined short flags and attached values cargo run --example short_flags -- -abcw10
help_text Auto-generated help cargo run --example help_text -- --help
error_handling Error handling patterns cargo run --example error_handling
version_flag Built-in version flag cargo run --example version_flag -- --version
env_fallback Environment variable fallback cargo run --example env_fallback -- --output out.txt
subcommands Git-style subcommands cargo run --example subcommands -- build --release
full_demo All features together cargo run --example full_demo -- -vj8 -o=result.txt input.txt

Contributing

Contributions are welcome. To get started:

  1. Fork the repository
  2. Create a feature branch (git checkout -b my-feature)
  3. Make your changes
  4. Run the tests: cargo test
  5. Submit a pull request

Please keep changes minimal and focused. This crate's goal is to stay small and dependency-free.

License

This project is licensed under the MIT License.