argvex — a CLI argument parser with a predictable output shape
Explore the API »
Report a bug
·
Request a feature
Most parsers guess your types for you.
app --verbose --count 5 -abc --no-ding --name hellominimist / mri — type roulette:
{
_: [],
verbose: true, // boolean
count: 5, // number
a: true, b: true, c: true, // booleans
ding: false, // negated boolean
name: "hello" // string
}
// Return type: string | number | boolean — depends on the valuearg — schema required, throws on unknowns:
// Must define every flag upfront with coercion functions
const args = arg({ "--verbose": Boolean, "--count": Number, "--name": String })
// Unknown flags throw: "Unknown or unexpected option: --no-ding"
// Return type: string | number | boolean — depends on the coercion functionargvex — every flag is string[], always:
{
_: [],
__: [],
verbose: [], // present, no value
count: [ "5" ], // string, your app decides if it's a number
a: [], b: [], c: [], // present, no value
"no-ding": [], // it's a flag called "no-ding", not magic
name: [ "hello" ] // string
}
// Return type: string[] — every flag, every timeNo guessing. No coercion. No typeof args.count === "number" || typeof args.count === "string" defensive checks.
You get a consistent shape, and your application decides what it means.
- Predictable output — every flag is
string[], every time. No type guessing, no coercion, no surprises. - Parse, don't interpret — argvex structures your argv. Your app decides what
--verboseor--count 5means. - Schema when you need it — works raw out of the box. Add a schema for aliases, arity control, and unknown-flag detection.
| 🚀 Zero dependencies | One file. Fast. No tree bloat. |
| 🔮 Predictable shape | Every flag is string[]. Always. |
| 🔌 Schema-optional | Works raw out of the box. Add constraints when you need them. |
| 🐚 UNIX philosophy | Do one thing well. Stay composable. |
| 💙 TypeScript | Full type inference from your schema. |
npm install argvexCall argvex() to get structured process.argv output.
brewer brew espresso --size medium --shots 3 --milk noneimport argvex from "argvex"
const args = argvex()
// args -> { _: [ "brewer", "brew", "espresso" ], __: [], size: [ "medium" ], shots: [ "3" ], milk: [ "none" ] }_ is the positionals array — commands, subcommands, file paths, bare words, and everything after --.
Note: Without a schema, every flag greedily consumes all following tokens until the next flag-like token (anything starting with -) or -- is encountered. This means --output file.txt input.txt will assign both file.txt and input.txt to the output flag, not treat input.txt as a positional. To control this, use a schema with arity — see Arity.
Standalone short flags and groups.
brewer brew americano -qs -m water -t 85import argvex from "argvex"
const args = argvex()
// args -> { _: [ "brewer", "brew", "americano" ], __: [], q: [], s: [], m: [ "water" ], t: [ "85" ] }Long flags and short flags both support = syntax.
brewer brew latte --size=large -m=oatimport argvex from "argvex"
const args = argvex()
// args -> { _: [ "brewer", "brew", "latte" ], __: [], size: [ "large" ], m: [ "oat" ] }This works with grouped flags too — the value is assigned to the last flag in the group.
brewer brew latte -ds=mediumimport argvex from "argvex"
const args = argvex()
// args -> { _: [ "brewer", "brew", "latte" ], __: [], d: [], s: [ "medium" ] }Use -- to separate flags from operands that might look like flags.
brewer brew --milk oat -- --not-a-flag latteimport argvex from "argvex"
const args = argvex()
// args -> { _: [ "brewer", "brew", "--not-a-flag", "latte" ], __: [], milk: [ "oat" ] }By default argvex reads from process.argv.slice(2). Pass argv to parse any string array — useful for testing, subcommand delegation, or piping parsed chunks between handlers.
import argvex from "argvex"
const args = argvex({ argv: ["--size", "large", "--shots", "2"] })
// args -> { _: [], __: [], size: [ "large" ], shots: [ "2" ] }Pass a schema to enable aliases, control arity, and detect unknown flags.
Map short flags to long flag names. Aliases accept exactly one character.
brewer brew mocha -d -m oat -c darkimport argvex from "argvex"
const schema = {
decaf: { alias: "d" },
milk: { alias: "m" },
chocolate: { alias: "c" }
}
const args = argvex({ schema })
// args -> { _: [ "brewer", "brew", "mocha" ], __: [], decaf: [], milk: [ "oat" ], chocolate: [ "dark" ] }arity controls how many values a flag consumes from the argument stream.
brewer brew -dsmedium -h2 macchiatoimport argvex from "argvex"
const schema = {
decaf: { alias: "d", arity: 0 },
size: { alias: "s", arity: 1 },
shots: { alias: "h", arity: 1 },
}
const args = argvex({ schema })
// args -> { _: [ "brewer", "brew", "macchiato" ], __: [], decaf: [], size: [ "medium" ], shots: [ "2" ] }Repeating a flag accumulates values — each invocation appends to the same key.
brewer brew flat-white --milk steamed --milk foamed --milk microfoamimport argvex from "argvex"
const schema = {
milk: { arity: 1 },
}
const args = argvex({ schema })
// args -> { _: [ "brewer", "brew", "flat-white" ], __: [], milk: [ "steamed", "foamed", "microfoam" ] }A single flag with higher arity consumes multiple tokens in one shot:
brewer brew flat-white --milk steamed foamed microfoamimport argvex from "argvex"
const schema = {
milk: { arity: 3 },
}
const args = argvex({ schema })
// args -> { _: [ "brewer", "brew", "flat-white" ], __: [], milk: [ "steamed", "foamed", "microfoam" ] }The = syntax (--flag=value) bypasses arity — the value is embedded in the token, not consumed from the stream. This means --verbose=true assigns "true" regardless of its arity setting — even arity: 0.
brewer brew espresso --verbose=trueimport argvex from "argvex"
const schema = {
verbose: { arity: 0 },
}
const args = argvex({ schema })
// args -> { _: [ "brewer", "brew", "espresso" ], __: [], verbose: [ "true" ] }When a schema is provided, any flag not defined in the schema is collected in __ as raw strings — dashes, = values and all. The parser does not consume any following arguments for unknown flags.
Without a schema, __ is always empty — every flag is accepted and parsed normally.
brewer brew espresso --verbose --output file.txt --format json -ximport argvex from "argvex"
const schema = {
verbose: { arity: 0 },
output: { arity: 1 },
}
const args = argvex({ schema })
// args -> { _: [ "brewer", "brew", "espresso", "json" ], __: [ "--format", "-x" ], verbose: [], output: [ "file.txt" ] }This gives you full control over how your CLI handles unknowns:
// Warn and continue
if (args.__.length) {
console.warn("Unknown flags:", args.__.join(", "))
}
// Fail hard
if (args.__.length) {
console.error("Unknown flags:", args.__.join(", "))
process.exit(1)
}
// Forward to another tool
if (args.__.length) {
spawnSync("other-tool", args.__, { stdio: "inherit" })
}Compare this to other parsers: minimist silently swallows unknowns into the result, mri terminates on them, and arg throws by default. argvex collects them separately so your app decides the policy.
argvex exports the following public types:
import argvex, { Schema, Options, ArgvEx, ParseError, ErrorCode } from "argvex"Schema — the shape of a schema definition. Use this to annotate a schema defined outside the argvex() call:
import argvex, { Schema } from "argvex"
const schema: Schema = {
size: { alias: "s", arity: 1 },
shots: { alias: "h", arity: 1 },
decaf: { alias: "d", arity: 0 },
}
const args = argvex({ schema })Options — the input shape passed to argvex():
import argvex, { Options } from "argvex"
const options: Options = {
argv: ["--size", "large"],
schema: { size: { arity: 1 } },
}
const args = argvex(options)ArgvEx — the return type of argvex(). Every flag is string[], plus _ for positionals and __ for unknown flags:
import argvex, { ArgvEx } from "argvex"
const args: ArgvEx = argvex()ParseError and ErrorCode — see Error handling.
argvex gives you raw parsed data — defaults, coercion, and validation are yours to define.
Flags without values are present as empty arrays. Check presence, not truthiness.
const args = argvex()
const verbose = args.verbose !== undefined
const debug = !!args.debugconst args = argvex()
if (!args.output) {
console.error("Missing required flag: --output")
process.exit(1)
}const args = argvex()
const milk = args.milk?.[0] ?? "steamed"
const shots = Number(args.shots?.[0] ?? "2")const args = argvex()
const ports = args.port?.map(Number) ?? []
const tags = args.tag ?? []Define it as a regular flag. No magic, no implicit boolean flip.
const schema = {
color: { arity: 0 },
"no-color": { arity: 0 },
}
const args = argvex({ schema })
const useColor = !args["no-color"]import argvex, { ParseError } from "argvex"
try {
const args = argvex()
// process args
} catch (error) {
if (error instanceof ParseError) {
console.error(error.code, error.argument)
process.exit(1)
}
throw error
}ParseError exposes two structured properties alongside the human-readable .message:
| Property | Type | Description |
|---|---|---|
.code |
ErrorCode |
Machine-readable error code (see below) |
.argument |
string |
The raw token or schema key that caused the error |
Error codes:
| Code | Thrown when |
|---|---|
INVALID_INPUT |
A malformed argument is encountered (e.g. ---triple-dash) |
INVALID_SCHEMA |
A schema definition is invalid (bad alias, reserved name, etc.) |
RESERVED_KEYWORD |
A flag named _ or __ is passed (conflicts with reserved keys) |
