Parsing Arguments
Real command-line tools take options (--watch), flags (-v), option values (--out dist or --out=dist), and positional arguments (the file to operate on). Turning the flat process.argv array into a structured, validated set of these by hand is tedious and error-prone — you have to handle =-joined values, combined short flags (-rf), the -- end-of-options separator, and defaults. Node 18.3+ ships a built-in parser, util.parseArgs, that handles the common cases with zero dependencies. This page covers the anatomy of CLI arguments, hand-parsing's pitfalls, parseArgs, and when to graduate to a full framework like Commander.
The vocabulary of CLI arguments
mytool build --watch -o dist --level=3 src/ -- --not-a-flag
^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^
│ │ │ │ │ │ everything after -- is
│ │ │ │ │ │ treated as POSITIONAL,
│ │ │ │ │ │ even if it looks like a flag
│ │ │ │ │ end-of-options separator
│ │ │ │ positional argument
│ │ │ long option with =value
│ │ short option (-o) with a value (dist)
│ boolean flag (present = true)
subcommandTerm | Example | Meaning |
|---|---|---|
Boolean flag |
| Present = on; takes no value |
Option with value |
| A named value (both forms common) |
Short flag |
| Single-letter; combinable: |
Positional |
| A bare argument, not attached to a flag |
Separator |
| Everything after is positional, never a flag |
Subcommand |
| A named mode ( |
Why not parse by hand?
// Naive hand-parsing — looks fine until the edge cases pile up:
const args = process.argv.slice(2)
const options = {}
for (let i = 0; i < args.length; i++) {
if (args[i] === '--out') options.out = args[++i] // space-separated value
else if (args[i].startsWith('--')) options[args[i].slice(2)] = true
// ...but what about --out=dist? combined -rf? the -- separator?
// negative numbers as values? = inside values? It snowballs fast.
}The built-in: util.parseArgs
import { parseArgs } from 'node:util' // built in, Node 18.3+
// mytool --watch --out dist src/index.js extra.js
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
watch: { type: 'boolean', short: 'w' }, // --watch or -w
out: { type: 'string', short: 'o', default: '.' }, // --out <value>
level: { type: 'string', multiple: true }, // collect repeats into an array
},
})
console.log(values) // { watch: true, out: 'dist', level: [...] }
console.log(positionals) // [ 'src/index.js', 'extra.js' ]{ watch: true, out: 'dist' }
[ 'src/index.js', 'extra.js' ]parseArgs vs a full framework
Need |
| Commander / yargs |
|---|---|---|
Dependency | None (built in) | External package |
Flags, valued options, positionals | Yes | Yes |
Auto-generated | No (write it yourself) | Yes |
Subcommands ( | No | Yes |
Validation / required / choices | Minimal | Rich |
Type coercion (numbers, etc.) | No — all strings | Yes |
Best for | Small/medium single-purpose tools | Multi-command, polished CLIs |