noargs
Use this skill when integrating noargs into Rust code. Focus on the crate's
actual API shape and usage patterns, not generic CLI-parsing background.
What this crate exposes
noargs::raw_args()— convenience wrapper aroundRawArgs::new(std::env::args())noargs::arg(name)→ArgSpec— positional argumentnoargs::opt(name)→OptSpec— named argument with value (--name VALUE)noargs::flag(name)→FlagSpec— named argument without value (--name)noargs::cmd(name)→CmdSpec— subcommandnoargs::HELP_FLAG— pre-built--help, -hflag specnoargs::VERSION_FLAG— pre-built--versionflag specnoargs::Result<T>andnoargs::Error- Runtime types — each an enum whose variants record how the value was
supplied:
Arg:Positional/Default/Example/NoneOpt:Long/Short/Env/Default/Example/MissingValue/NoneFlag:Long/Short/Env/NoneCmd:Some/None
The imperative parsing loop
The crate is built around one pattern — there is no declarative schema:
- Build
RawArgsfrom argv (noargs::raw_args()). - Set help metadata (
args.metadata_mut().app_name = …,app_description = …). - Consume
VERSION_FLAGfirst and exit if present. - Consume
HELP_FLAGwithtake_help— this setshelp_mode/full_helpin metadata so subsequenttakecalls yield example/default values rather than the real input. - Call
.take(&mut args)on each spec to consume matching tokens from the buffer..take()mutatesRawArgs; tokens are removed as they are claimed. - Call
args.finish()?at the end — in help mode it returnsOk(Some(help_text)); otherwise it errors on unclaimed tokens or missing subcommand.
Choosing the right API
- Fixed positional —
arg("<NAME>")for required,arg("[NAME]")for optional. Use<>/[]purely as a naming convention; they do not drive behavior — required-ness comes from calling.then()(required) vs.present_and_then()(optional) or setting.default(). - Named option with value —
opt("long-name").short('f').ty("VALUE"). Short-only options are not supported; every option must have a long name. - Boolean flag —
flag("long-name").short('f')..is_present()is the standard reader. Likeopt, short-only flags are not supported. - Subcommand —
cmd("name"). Only matches if it's the next unconsumed token. Consume global flags/opts beforecmd().take(), or the command token will be shadowed by leading flag tokens. - Environment fallback —
opt(...).env("VAR")for options,flag(...).env("VAR")for flags. Empty-string values are ignored. - Required-in-help — add
.example("…")to any required option / positional so help-mode output has something to print. Optional fields with.default()or.present_and_then()do not need.example()..example()is consulted only whenhelp_modeis on; it never affects parsing of real argv. Required-ness is decided by.then()(errors if absent) vs.present_and_then()(yieldsOption<T>); add.default()to give.then()a value to bind when the field is absent.
Usage gotchas
- Order matters. Call
flag()/opt().take()beforearg().take().arg()eagerly consumes the first remaining token — including a leftover-v(declared flag taken too late) or--bogus(typo / undeclared flag).args.finish()reports unconsumed tokens asError::UnexpectedArg, but a typo that fits an empty positional slot is bound silently — e.g.prog --format json --bogusleaves--bogusas the<INPUT>value with no error. take_helpvstake.HELP_FLAG.take_help(&mut args)flipsmetadata.help_mode = truewhen-h/--helpis present, and also setsfull_help = truefor--help(long form). Usetake_help, not plaintake, for the help flag.- Help mode collapses
take. Oncehelp_modeis on,ArgSpec::take/OptSpec::takereturnDefault/Example/Nonevariants without scanning argv. You still need to call each.take()so it appears in the help log; but afterargs.finish()?returnsSome(help), print it and return early. Guard application logic behindif args.metadata().help_mode { return … }inside subcommand handlers that early-return beforefinish(). .then(f)is "required". It returnsErr(Error::MissingArg/MissingOpt)when the value isn't present, so use it for required args or for required parsing after.default(). Use.present_and_then(f)for optional values — it returnsOk(None)when absent.- Repeated same-name options / positional arrays. Call
.take()in awhile let Some(v) = spec.take(&mut args).present_and_then(…)? { … }loop. Each iteration consumes one match. - Option value forms. Long:
--name=valueor--name value. Short:-fvalue(concatenated) or-f value(separate). A bare-fwith no following token yieldsOpt::MissingValue—.then()surfaces this asError::MissingOpt. - Short flag packing.
-abcexpands to flagsa,b,cwhen every char passesmetadata.is_valid_flag_chars(default: ASCII alphabetic). If an app uses short options whose values may be alphabetic (e.g.-khello), tightenis_valid_flag_charsto an allow-list of actual flag letters. Erroris terminal-only. It deliberately does not implementstd::error::ErrororDisplay; onlyDebug. Return it frommainvianoargs::Result<()>. AnyDisplay-able error converts intoErrorvia the blanketFromimpl, so?works withparse(),io::Error, etc.cmdstops at the first unconsumed token. If flags haven't been consumed yet, a subcommand that sits after them won't be found. Consume globalflag/optfirst, thencmd.default("…")takes&'static str. For runtime-computed defaults, use astatic LazyLock<String>and pass&*LAZY.
API patterns to preserve
Minimal top-level CLI:
fn main() -> noargs::Result<()> {
let mut args = noargs::raw_args();
args.metadata_mut().app_name = env!("CARGO_PKG_NAME");
args.metadata_mut().app_description = env!("CARGO_PKG_DESCRIPTION");
if noargs::VERSION_FLAG.take(&mut args).is_present() {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
return Ok(());
}
noargs::HELP_FLAG.take_help(&mut args);
// flags / opts BEFORE positional args
let verbose = noargs::flag("verbose").short('v').take(&mut args).is_present();
let retries: usize = noargs::opt("retries")
.short('r').ty("N").default("1")
.take(&mut args)
.then(|o| o.value().parse())?;
let format: String = noargs::opt("format")
.ty("FORMAT").example("json") // required → needs example()
.take(&mut args)
.then(|o| o.value().parse())?;
let input: String = noargs::arg("<INPUT>")
.example("input.txt") // required → needs example()
.take(&mut args)
.then(|a| a.value().parse())?;
let output: Option<String> = noargs::arg("[OUTPUT]")
.take(&mut args)
.present_and_then(|a| a.value().parse())?;
if let Some(help) = args.finish()? {
print!("{help}");
return Ok(());
}
// application logic using verbose / retries / format / input / output
let _ = (verbose, retries, format, input, output);
Ok(())
}
Repeated same-name option and positional array:
let include_spec = noargs::opt("include").short('I').ty("PATH");
let mut includes = Vec::<String>::new();
while let Some(path) = include_spec
.take(&mut args)
.present_and_then(|o| o.value().parse())?
{
includes.push(path);
}
let first: String = noargs::arg("<INPUT>")
.example("a.txt")
.take(&mut args)
.then(|a| a.value().parse())?;
let rest_spec = noargs::arg("[INPUT]...");
let mut inputs = vec![first];
while let Some(v) = rest_spec
.take(&mut args)
.present_and_then(|a| a.value().parse())?
{
inputs.push(v);
}
Subcommand routing with per-command scope:
fn main() -> noargs::Result<()> {
let mut args = noargs::raw_args();
args.metadata_mut().app_name = env!("CARGO_PKG_NAME");
args.metadata_mut().app_description = env!("CARGO_PKG_DESCRIPTION");
if noargs::VERSION_FLAG.take(&mut args).is_present() {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
return Ok(());
}
noargs::HELP_FLAG.take_help(&mut args);
let _ = try_run_hello(&mut args)? || try_run_sum(&mut args)?;
if let Some(help) = args.finish()? {
print!("{help}");
}
Ok(())
}
fn try_run_hello(args: &mut noargs::RawArgs) -> noargs::Result<bool> {
if !noargs::cmd("hello").doc("Print a greeting").take(args).is_present() {
return Ok(false);
}
let loud = noargs::flag("loud").short('l').take(args).is_present();
let name: String = noargs::arg("<NAME>")
.example("Alice")
.take(args)
.then(|a| a.value().parse())?;
// IMPORTANT: skip real work while building help text.
if args.metadata().help_mode { return Ok(true); }
let msg = format!("Hello, {name}!");
println!("{}", if loud { msg.to_uppercase() } else { msg });
Ok(true)
}
Custom validation via .then:
let port: u16 = noargs::opt("port").short('p').default("8080")
.take(&mut args)
.then(|o| -> Result<_, Box<dyn std::error::Error>> {
let n: u16 = o.value().parse()?;
if n == 0 { return Err("port must be > 0".into()); }
Ok(n)
})?;
Environment-variable fallbacks:
let endpoint: String = noargs::opt("endpoint")
.env("MYAPP_ENDPOINT")
.default("http://localhost:8080")
.take(&mut args)
.then(|o| o.value().parse())?;
let debug = noargs::flag("debug").env("MYAPP_DEBUG").take(&mut args).is_present();
Runnable references
The repo's examples/ directory contains runnable templates that demonstrate
complete patterns — read them end-to-end when a snippet isn't enough:
examples/basics.rs— single-command CLI with flags, options, env fallbacks, required + optional positionals, and a--dry-runpath. Comments flag the common order /example()/ help-mode pitfalls.examples/arrays.rs— repeatedoptand positional-array patterns usingwhile let Some(v) = spec.take(...).present_and_then(...)? { ... }, plus the<NAME>/[NAME]...naming convention.examples/subcommands.rs—try_run_*command routing pattern (bool short-circuit chain), per-command scoped flags/args, and theif args.metadata().help_mode { return Ok(true); }guard inside handlers.
Practical hints
- Start by copying the structure of
examples/basics.rsorexamples/subcommands.rsinstead of re-deriving the order-sensitive steps (metadata → version → help → opts/flags → args →finish). - For any required field, set
.example("…")so--helpoutput is not blank. - For any optional field, prefer
.default("…")+.then()when a sensible default exists; reach for.present_and_then()+Option<T>only when the field really is absent-vs-present. - Don't try to match options with only a short name — the crate doesn't
support it. Give every
opt/flaga long name, with.short('x')optional. - When
finish()reportsUndefinedCommandunexpectedly, the real cause is usually a leading flag/opt not yet consumed — consume global flags first, then callcmd(...).take(). - The crate is std-only (uses
std::env,std::io::IsTerminal); it does not targetno_std. - Values are parsed with
str::parsethrough.then()— anyFromStrtype works, and anyDisplayerror is turned intoError::InvalidArg/InvalidOptwith a helpful message.
Source: sile/noargs — distributed by TomeVault.