NodeJSParsing Arguments

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

Text
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)
       subcommand

Term

Example

Meaning

Boolean flag

--watch, -v

Present = on; takes no value

Option with value

--out dist, --out=dist

A named value (both forms common)

Short flag

-v

Single-letter; combinable: -rf = -r -f

Positional

src/, file.txt

A bare argument, not attached to a flag

Separator

--

Everything after is positional, never a flag

Subcommand

build, init

A named mode (git commit, npm install)

Arguments come in a handful of shapes — flags, valued options, positionals, the `--` separator, and subcommands
Before parsing, it helps to name the parts. A **boolean flag** (`--watch`) is on simply by being present. An **option with a value** names a setting and takes a value, written either space-separated (`--out dist`) or `=`-joined (`--out=dist`) — a good parser accepts both. **Short flags** are single letters (`-v`) and conventionally combinable (`-rf` means `-r -f`). **Positional arguments** are bare values not attached to any flag (the files to process). The **`--` separator** means "everything after this is positional, even if it starts with a dash" — essential when a filename legitimately begins with `-`. And **subcommands** (`git commit`) select a mode. Knowing these shapes is what tells you whether a quick hand-parse suffices or you need a real parser.
Why not parse by hand?

JS
// 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.
}
Hand-rolled argument parsing breaks on the edge cases — `=`-values, combined flags, `--`, and negatives — use a parser
A 10-line loop handles the happy path and then collapses under the cases real users hit: `--out=dist` (the `=` form), `-rf` (combined short flags), `--` (the end-of-options separator), values that look like flags (`--out -5`), and repeated options (`--include a --include b`). Each one you patch makes the loop more tangled and more likely to mis-parse, and bugs here are *confusing* to users because the tool silently does the wrong thing. Don't reinvent this — Node's built-in `util.parseArgs` covers the standard cases for free, and full frameworks ([Commander](/nodejs/commander), yargs) cover everything plus help text, subcommands, and validation. Reserve hand-parsing for the most trivial one-flag scripts.
The built-in: util.parseArgs

JS
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' ]
`util.parseArgs` is the zero-dependency built-in — declare your options as `boolean`/`string`, get `values` + `positionals` back
Since Node 18.3, **`util.parseArgs`** gives you structured parsing without a dependency. You declare each option's `type` (`'boolean'` or `'string'`), an optional `short` alias, a `default`, and whether it can repeat (`multiple: true` collects all occurrences into an array). It returns `values` (an object of parsed options) and `positionals` (the bare arguments, when `allowPositionals` is set), and it correctly handles `=`-joined values, combined short flags, and the `--` separator. What it deliberately *doesn't* do: generate `--help` text, validate values beyond type, support subcommands, or coerce numbers (every value is a string — you convert yourself). That minimalism is fine for small-to-medium tools; it's the sweet spot between hand-parsing and a full framework.
parseArgs vs a full framework

Need

util.parseArgs

Commander / yargs

Dependency

None (built in)

External package

Flags, valued options, positionals

Yes

Yes

Auto-generated --help

No (write it yourself)

Yes

Subcommands (git commit)

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

Reach for `parseArgs` on simple tools; switch to Commander when you need subcommands, help text, and validation
The decision is about scope. For a **single-purpose tool** with a handful of flags, `util.parseArgs` is ideal — no dependency, and you keep full control (you'll just hand-write the `--help` string and any number coercion). The moment you need **subcommands** (`mytool init`, `mytool build`), **auto-generated help**, **required options**, **choices/validation**, or coerced types, hand-maintaining that on top of `parseArgs` becomes its own little framework — at which point [**Commander**](/nodejs/commander) (or yargs) earns its dependency by giving you all of it declaratively, with conventions users already know. Start small; graduate when the tool grows a second command.
Next
The most popular full-featured CLI framework: [Commander.js](/nodejs/commander).