Skip to content

Commit 0482b28

Browse files
Benjamin NewmanBenjamin Newman
authored andcommitted
docs: add util.parseArgs replacements for CLI argument parsers
Adds migration documentation for replacing minimist, mri, arg, meow, yargs-parser, yargs, commander, and sade with Node.js built-in util.parseArgs (available in Node 18.3+/16.17+).
1 parent 0b50a32 commit 0482b28

2 files changed

Lines changed: 435 additions & 0 deletions

File tree

docs/modules/parseargs.md

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
---
2+
description: Modern alternatives to CLI argument parsing packages using Node.js built-in util.parseArgs
3+
---
4+
5+
# Replacements for argument parsers
6+
7+
Node.js 18.3+ (and 16.17+) includes [`util.parseArgs`](https://nodejs.org/api/util.html#utilparseargsconfig), a built-in argument parser that can replace many common CLI parsing libraries.
8+
9+
## Packages covered
10+
11+
- `minimist`
12+
- `mri`
13+
- `arg`
14+
- `meow`
15+
- `yargs-parser`
16+
- `yargs`
17+
- `commander`
18+
- `sade`
19+
20+
## `minimist`
21+
22+
`minimist` is the simplest migration case. It's often a transitive dependency rather than a direct choice:
23+
24+
```ts
25+
import minimist from 'minimist' // [!code --]
26+
import { parseArgs } from 'node:util' // [!code ++]
27+
28+
const argv = minimist(process.argv.slice(2)) // [!code --]
29+
const { values, positionals } = parseArgs({ // [!code ++]
30+
args: process.argv.slice(2), // [!code ++]
31+
options: { // [!code ++]
32+
force: { type: 'boolean', short: 'f' }, // [!code ++]
33+
output: { type: 'string', short: 'o' }, // [!code ++]
34+
}, // [!code ++]
35+
allowPositionals: true, // [!code ++]
36+
}) // [!code ++]
37+
38+
// Access options
39+
argv.force // [!code --]
40+
values.force // [!code ++]
41+
42+
// Access positionals
43+
argv._ // [!code --]
44+
positionals // [!code ++]
45+
```
46+
47+
### Handling unknown options
48+
49+
`minimist` accepts any flag by default. To match this behavior, use `strict: false`:
50+
51+
```ts
52+
const { values, positionals } = parseArgs({
53+
args: process.argv.slice(2),
54+
strict: false,
55+
allowPositionals: true,
56+
})
57+
```
58+
59+
### Providing a minimist-compatible interface
60+
61+
For gradual migration, you can create a compatibility layer:
62+
63+
```ts
64+
import { parseArgs } from 'node:util'
65+
66+
const { values, positionals } = parseArgs({
67+
args: process.argv.slice(2),
68+
options: {
69+
help: { type: 'boolean', short: 'h' },
70+
force: { type: 'boolean', short: 'f' },
71+
},
72+
strict: false,
73+
allowPositionals: true,
74+
})
75+
76+
// minimist-compatible object
77+
const argv = {
78+
_: positionals,
79+
...values,
80+
// Add short aliases only when truthy (minimist behavior)
81+
...(values.help && { h: values.help }),
82+
...(values.force && { f: values.force }),
83+
}
84+
```
85+
86+
### Using tokens for advanced parsing
87+
88+
For minimist-style parsing where `--flag value` treats `value` as the flag's argument (not a positional), use the `tokens` option:
89+
90+
```ts
91+
const { tokens } = parseArgs({
92+
args: process.argv.slice(2),
93+
strict: false,
94+
allowPositionals: true,
95+
tokens: true,
96+
})
97+
98+
const result = { _: [] }
99+
for (let i = 0; i < tokens.length; i++) {
100+
const token = tokens[i]
101+
if (token.kind === 'option') {
102+
const nextToken = tokens[i + 1]
103+
// Check if boolean flag is followed by a value
104+
if (token.value === undefined && nextToken?.kind === 'positional') {
105+
result[token.name] = nextToken.value
106+
i++ // Skip next token
107+
} else {
108+
result[token.name] = token.value ?? true
109+
}
110+
} else if (token.kind === 'positional') {
111+
result._.push(token.value)
112+
}
113+
}
114+
```
115+
116+
## `mri`
117+
118+
`mri` is a lightweight minimist alternative. The migration is nearly identical to minimist:
119+
120+
```ts
121+
import mri from 'mri' // [!code --]
122+
import { parseArgs } from 'node:util' // [!code ++]
123+
124+
const argv = mri(process.argv.slice(2), { // [!code --]
125+
alias: { h: 'help', v: 'version' }, // [!code --]
126+
boolean: ['help', 'version'], // [!code --]
127+
}) // [!code --]
128+
const { values, positionals } = parseArgs({ // [!code ++]
129+
args: process.argv.slice(2), // [!code ++]
130+
options: { // [!code ++]
131+
help: { type: 'boolean', short: 'h' }, // [!code ++]
132+
version: { type: 'boolean', short: 'v' }, // [!code ++]
133+
}, // [!code ++]
134+
allowPositionals: true, // [!code ++]
135+
}) // [!code ++]
136+
```
137+
138+
## `arg`
139+
140+
`arg` uses a schema-based approach similar to `parseArgs`:
141+
142+
```ts
143+
import arg from 'arg' // [!code --]
144+
import { parseArgs } from 'node:util' // [!code ++]
145+
146+
const args = arg({ // [!code --]
147+
'--port': Number, // [!code --]
148+
'--host': String, // [!code --]
149+
'--verbose': Boolean, // [!code --]
150+
'-p': '--port', // [!code --]
151+
'-h': '--host', // [!code --]
152+
}) // [!code --]
153+
const { values } = parseArgs({ // [!code ++]
154+
args: process.argv.slice(2), // [!code ++]
155+
options: { // [!code ++]
156+
port: { type: 'string', short: 'p' }, // [!code ++]
157+
host: { type: 'string', short: 'h' }, // [!code ++]
158+
verbose: { type: 'boolean' }, // [!code ++]
159+
}, // [!code ++]
160+
}) // [!code ++]
161+
162+
// Note: parseArgs returns strings, convert if needed
163+
const port = Number(values.port) // [!code ++]
164+
```
165+
166+
> [!NOTE]
167+
> `parseArgs` only supports `string` and `boolean` types. For numbers, parse the string value yourself.
168+
169+
## `meow`
170+
171+
`meow` is popular for small CLIs, combining parsing with auto-help from package.json:
172+
173+
```ts
174+
import meow from 'meow' // [!code --]
175+
import { parseArgs } from 'node:util' // [!code ++]
176+
import { readFileSync } from 'node:fs' // [!code ++]
177+
178+
const cli = meow(` // [!code --]
179+
Usage // [!code --]
180+
$ my-cli <input> // [!code --]
181+
182+
Options // [!code --]
183+
--rainbow, -r Include a rainbow // [!code --]
184+
--postfix Append a string // [!code --]
185+
`, { // [!code --]
186+
importMeta: import.meta, // [!code --]
187+
flags: { // [!code --]
188+
rainbow: { type: 'boolean', shortFlag: 'r' }, // [!code --]
189+
postfix: { type: 'string', default: '!' }, // [!code --]
190+
} // [!code --]
191+
}) // [!code --]
192+
cli.input // => positionals // [!code --]
193+
cli.flags // => { rainbow: false, postfix: '!' } // [!code --]
194+
195+
const { values, positionals } = parseArgs({ // [!code ++]
196+
args: process.argv.slice(2), // [!code ++]
197+
options: { // [!code ++]
198+
rainbow: { type: 'boolean', short: 'r' }, // [!code ++]
199+
postfix: { type: 'string' }, // [!code ++]
200+
help: { type: 'boolean', short: 'h' }, // [!code ++]
201+
version: { type: 'boolean', short: 'v' }, // [!code ++]
202+
}, // [!code ++]
203+
allowPositionals: true, // [!code ++]
204+
}) // [!code ++]
205+
const postfix = values.postfix ?? '!' // [!code ++]
206+
207+
// Handle --help yourself // [!code ++]
208+
if (values.help) { // [!code ++]
209+
console.log(` // [!code ++]
210+
Usage // [!code ++]
211+
$ my-cli <input> // [!code ++]
212+
213+
Options // [!code ++]
214+
--rainbow, -r Include a rainbow // [!code ++]
215+
--postfix Append a string // [!code ++]
216+
`) // [!code ++]
217+
process.exit(0) // [!code ++]
218+
} // [!code ++]
219+
220+
// Handle --version yourself // [!code ++]
221+
if (values.version) { // [!code ++]
222+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) // [!code ++]
223+
console.log(pkg.version) // [!code ++]
224+
process.exit(0) // [!code ++]
225+
} // [!code ++]
226+
```
227+
228+
> [!NOTE]
229+
> `meow` provides automatic `--help` and `--version` handling from your package.json. With `parseArgs`, you implement these yourself. For very simple CLIs, this trade-off may not be worth it.
230+
231+
## `yargs-parser`
232+
233+
`yargs-parser` (the parsing engine behind `yargs`) has more features, but basic usage maps directly:
234+
235+
```ts
236+
import yargsParser from 'yargs-parser' // [!code --]
237+
import { parseArgs } from 'node:util' // [!code ++]
238+
239+
const argv = yargsParser(process.argv.slice(2), { // [!code --]
240+
alias: { h: 'help' }, // [!code --]
241+
boolean: ['help', 'verbose'], // [!code --]
242+
string: ['config'], // [!code --]
243+
}) // [!code --]
244+
const { values, positionals } = parseArgs({ // [!code ++]
245+
args: process.argv.slice(2), // [!code ++]
246+
options: { // [!code ++]
247+
help: { type: 'boolean', short: 'h' }, // [!code ++]
248+
verbose: { type: 'boolean' }, // [!code ++]
249+
config: { type: 'string' }, // [!code ++]
250+
}, // [!code ++]
251+
allowPositionals: true, // [!code ++]
252+
}) // [!code ++]
253+
```
254+
255+
## `yargs`
256+
257+
`yargs` uses a chained builder API. For simple cases without subcommands:
258+
259+
```ts
260+
import yargs from 'yargs' // [!code --]
261+
import { hideBin } from 'yargs/helpers' // [!code --]
262+
import { parseArgs } from 'node:util' // [!code ++]
263+
264+
const argv = yargs(hideBin(process.argv)) // [!code --]
265+
.option('port', { // [!code --]
266+
alias: 'p', // [!code --]
267+
type: 'number', // [!code --]
268+
default: 3000, // [!code --]
269+
}) // [!code --]
270+
.option('host', { // [!code --]
271+
alias: 'h', // [!code --]
272+
type: 'string', // [!code --]
273+
default: 'localhost', // [!code --]
274+
}) // [!code --]
275+
.option('verbose', { // [!code --]
276+
type: 'boolean', // [!code --]
277+
default: false, // [!code --]
278+
}) // [!code --]
279+
.parseSync() // [!code --]
280+
281+
const { values } = parseArgs({ // [!code ++]
282+
args: process.argv.slice(2), // [!code ++]
283+
options: { // [!code ++]
284+
port: { type: 'string', short: 'p' }, // [!code ++]
285+
host: { type: 'string', short: 'h' }, // [!code ++]
286+
verbose: { type: 'boolean' }, // [!code ++]
287+
}, // [!code ++]
288+
}) // [!code ++]
289+
const port = Number(values.port ?? '3000') // [!code ++]
290+
const host = values.host ?? 'localhost' // [!code ++]
291+
const verbose = values.verbose ?? false // [!code ++]
292+
```
293+
294+
### yargs with subcommands
295+
296+
`yargs` subcommand support cannot be directly replaced with `parseArgs`. You'll need to handle routing yourself:
297+
298+
```ts
299+
// yargs approach // [!code --]
300+
yargs(hideBin(process.argv)) // [!code --]
301+
.command('serve', 'Start the server', (yargs) => { // [!code --]
302+
return yargs.option('port', { type: 'number' }) // [!code --]
303+
}, (argv) => { // [!code --]
304+
startServer(argv.port) // [!code --]
305+
}) // [!code --]
306+
.command('build', 'Build the project', {}, () => { // [!code --]
307+
runBuild() // [!code --]
308+
}) // [!code --]
309+
.parse() // [!code --]
310+
311+
// parseArgs approach // [!code ++]
312+
const { values, positionals } = parseArgs({ // [!code ++]
313+
args: process.argv.slice(2), // [!code ++]
314+
options: { // [!code ++]
315+
port: { type: 'string' }, // [!code ++]
316+
}, // [!code ++]
317+
allowPositionals: true, // [!code ++]
318+
}) // [!code ++]
319+
320+
const [command] = positionals // [!code ++]
321+
switch (command) { // [!code ++]
322+
case 'serve': // [!code ++]
323+
startServer(Number(values.port)) // [!code ++]
324+
break // [!code ++]
325+
case 'build': // [!code ++]
326+
runBuild() // [!code ++]
327+
break // [!code ++]
328+
default: // [!code ++]
329+
console.error(`Unknown command: ${command}`) // [!code ++]
330+
process.exit(1) // [!code ++]
331+
} // [!code ++]
332+
```
333+
334+
> [!NOTE]
335+
> If your CLI relies heavily on yargs features like `.demandOption()`, `.conflicts()`, `.implies()`, auto-generated help with `--help`, or complex subcommand nesting, migrating to `parseArgs` requires implementing these features yourself. Evaluate whether the dependency savings justify the added code.
336+
337+
## `commander` and `sade`
338+
339+
`commander` and `sade` provide subcommand routing and auto-generated help, which `parseArgs` does not. For simple single-command CLIs, you can replace the parsing portion:
340+
341+
```ts
342+
import { program } from 'commander' // [!code --]
343+
program // [!code --]
344+
.option('-f, --force', 'Force operation') // [!code --]
345+
.option('-o, --output <path>', 'Output path') // [!code --]
346+
.parse() // [!code --]
347+
const opts = program.opts() // [!code --]
348+
349+
import { parseArgs } from 'node:util' // [!code ++]
350+
const { values } = parseArgs({ // [!code ++]
351+
args: process.argv.slice(2), // [!code ++]
352+
options: { // [!code ++]
353+
force: { type: 'boolean', short: 'f' }, // [!code ++]
354+
output: { type: 'string', short: 'o' }, // [!code ++]
355+
}, // [!code ++]
356+
}) // [!code ++]
357+
```
358+
359+
> [!NOTE]
360+
> If you need subcommands, auto-generated help, or validation, `parseArgs` may not be sufficient on its own. Consider keeping `commander` or `sade` for complex CLIs, or build these features yourself.
361+
362+
## Feature comparison
363+
364+
| Feature | `parseArgs` | `minimist` | `yargs` | `commander` |
365+
|---------|-------------|------------|---------|-------------|
366+
| Boolean flags |||||
367+
| String options |||||
368+
| Short aliases |||||
369+
| Multiple values |`multiple: true` ||||
370+
| Default values | ⚠️ manual `??` ||||
371+
| Subcommands |||||
372+
| Auto-help |||||
373+
| Type coercion | ❌ (string/boolean only) ||||
374+
| Validation |||||
375+
376+
## Node.js version requirements
377+
378+
`util.parseArgs` is available in:
379+
- Node.js 18.3.0+
380+
- Node.js 16.17.0+
381+
382+
For older Node.js versions, use the [`@pkgjs/parseargs`](https://github.com/pkgjs/parseargs) polyfill.
383+
384+
## Further reading
385+
386+
- [Node.js util.parseArgs documentation](https://nodejs.org/api/util.html#utilparseargsconfig)
387+
- [parseArgs proposal and discussion](https://github.com/nodejs/node/pull/42675)

0 commit comments

Comments
 (0)