mrkeyoor.com_
Wed 05 Aug 19:52 UTC
npmCLI & Toolingupdated 05 Aug 2026

yargs

yargs parses command line arguments into an object and builds the interactive parts of a CLI around that: subcommands with their own options, an auto-generated help screen, validation, and Bash/Zsh completion scripts. You chain configuration calls (.command(), .option(), .demandCommand(), .strict()) on a parser instance and call .parse() to get argv. It has been one of the two default Node argument parsers for over a decade (mocha is built on it), and version 18 made it ESM-only with a Node 20.19+ floor.

Verdict

Feature-wise still the most complete argument parser on npm, and the right call for complex ESM CLIs that want completion and config layering. For CJS projects, small tools, or type-strict teams, commander or the built-in parseArgs gets you there with less baggage.

API stability4/5The chaining API has been recognizable since v12 or so; v18's breakage was packaging (ESM-only, Node 20.19+ floor) rather than API redesign, and v17 held stable from 2021 to 2025.
Docs3/5api.md in the repo covers every method and yargs.js.org exists, but examples mix eras, TypeScript guidance is thin, and discovering the right combination of strict/demand/fail options takes trial and error.
Maintenance3/5Pushed July 2026 and 18.1.0 shipped that month, but the cadence is slow (14 months between 18.0.0 and 18.1.0) and 231 issues and PRs sit open; the original core team is mostly elsewhere.
Ecosystem5/5About 245M weekly downloads, mocha and a large share of published CLIs depend on it, and a decade of Stack Overflow answers cover nearly every scenario.

Use it if

  • You are building a multi-command CLI (tool serve, tool build) and want per-command options, positional arguments, and grouped help generated for you
  • You want parsing conveniences out of the box: type coercion, --no-flag negation, camelCase aliases, array options, and count options like -vvv
  • You need shell completion: .completion() generates Bash and Zsh scripts, which commander does not do without a plugin
  • You want configuration layering: .env() prefixed environment variables, .config() JSON files, and defaults all merge into one argv with a documented precedence
Skip it if

Setup reality

npm install yargs, then import yargs from 'yargs' and hideBin from 'yargs/helpers'; the yargs(hideBin(process.argv)) incantation is boilerplate everyone copies. On v18 your package must be ESM or you must use dynamic import; there is no CJS build. TypeScript users install @types/yargs separately and discover that typing .command() builders properly takes real effort. Node below 20.19 refuses to run it, which matters on older CI images. The docs are markdown files in the repo, and some examples still show v17 idioms.

Patterns

Parse flags into argvbasic-parse

#!/usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

const argv = yargs(hideBin(process.argv)).parse()
// ./tool.js --port 8080 --verbose  ->  { port: 8080, verbose: true }

hideBin strips the node binary and script path and handles Electron quirks. Values that look numeric are coerced to numbers automatically.

Declare typed options with validationdefine-options

const argv = yargs(hideBin(process.argv))
  .option('env', {
    alias: 'e',
    type: 'string',
    choices: ['dev', 'staging', 'prod'],
    default: 'dev',
    describe: 'target environment'
  })
  .option('force', { type: 'boolean', default: false })
  .parse()

Dashed flags appear twice in argv: --dry-run gives both argv['dry-run'] and argv.dryRun. choices rejects bad values before your code runs.

Build git-style subcommandssubcommands

yargs(hideBin(process.argv))
  .command(
    'serve [port]',
    'start the server',
    (yargs) => yargs.positional('port', { describe: 'port to bind', default: 5000 }),
    (argv) => serve(argv.port)
  )
  .command('build', 'compile the project', {}, (argv) => build(argv))
  .demandCommand(1, 'pick a command')
  .parse()

Without demandCommand, running with no command silently does nothing; the second argument is the error message users see.

Reject typos and unknown flagsstrict-mode

yargs(hideBin(process.argv))
  .command('deploy', 'ship it', {}, deploy)
  .strict()          // unknown commands AND options error out
  .strictOptions()   // or: only unknown options
  .parse()

Off by default, so --verbos (typo) is accepted silently unless you call strict(); nearly every real CLI wants this.

Use async handlers correctlyasync-command-handler

await yargs(hideBin(process.argv))
  .command('migrate', 'run migrations', {}, async (argv) => {
    await runMigrations(argv)
  })
  .parseAsync()

Use parseAsync() when any handler returns a promise; plain parse() returns before async handlers settle, so rejections can escape as unhandled.

Read options from environment variablesenv-variables

yargs(hideBin(process.argv))
  .env('MYAPP') // MYAPP_API_KEY -> argv.apiKey
  .option('api-key', { type: 'string', demandOption: true })
  .parse()

Precedence is command line over env over config file over defaults; demandOption is satisfied by the env var, which is what you want in CI.

Load options from a JSON config fileconfig-file

yargs(hideBin(process.argv))
  .config('settings') // --settings ./deploy.json merges its keys into argv
  .option('region', { type: 'string', default: 'us-east-1' })
  .parse()

Keys from the file are treated like typed options; explicit command line flags still win over file values.

Normalize argv before handlers runmiddleware

yargs(hideBin(process.argv))
  .middleware((argv) => {
    if (argv.verbose) process.env.LOG_LEVEL = 'debug'
    argv.startedAt = Date.now()
  })
  .command('run', 'run the thing', {}, (argv) => run(argv))
  .parse()

Middleware runs after validation by default; pass true as the second argument to run it before validation (for example to fill required options).

Generate Bash/Zsh completionshell-completion

yargs(hideBin(process.argv))
  .command('serve', 'start server', {}, serve)
  .completion() // adds a hidden `completion` command
  .parse()
// user runs: ./tool completion >> ~/.bashrc

Completion covers command and flag names automatically; for dynamic values provide a function to completion(cmd, describe, fn).

Control error output and exit behaviorfail-handler

yargs(hideBin(process.argv))
  .command('run', 'run', {}, run)
  .fail((msg, err, yargs) => {
    if (err) throw err // real exceptions propagate
    console.error(yargs.help())
    console.error('\nError:', msg)
    process.exit(64)
  })
  .parse()

Default behavior prints the message and exits 1. Distinguish msg (validation failure) from err (thrown in a handler) or you will swallow stack traces.

Test parsing without exiting the processtesting-a-cli

import { buildParser } from '../src/cli.js'

test('serve parses port', async () => {
  const argv = await buildParser()
    .exitProcess(false)
    .parseAsync(['serve', '--port', '3000'])
  expect(argv.port).toBe(3000)
})

exitProcess(false) makes failures throw instead of calling process.exit; export a factory that builds the parser so tests and the bin script share it.

Alternatives

PackageRegistryPick it when
commandernpmYou want the same command-and-option feature set with zero dependencies, bundled types, and CJS plus ESM support.
cacnpmYou want a tiny dependency-free parser with a commander-like API for small-to-medium CLIs.
cittynpmYou are in the unjs/Nitro ecosystem and want declarative command definitions with lazy-loaded subcommands.