NodeJSCommand-Line Arguments (process.argv)

Command-Line Arguments (process.argv)

Command-line tools take input as arguments. Node exposes them as process.argv — a plain array of strings, assembled once at startup from what the shell handed your process. Understanding its exact layout, how the shell mangles your input before Node ever sees it, and how to parse it robustly is the foundation of every CLI you will build.

The shape of process.argv

args.js

JS
console.log(process.argv)

Bash
node args.js hello world --verbose
[
  '/usr/local/bin/node',   // [0] absolute path to the Node executable
  '/path/to/args.js',      // [1] absolute path to the script being run
  'hello',                 // [2] your first argument
  'world',                 // [3]
  '--verbose'              // [4]
]
Your args start at index 2
Indexes 0 and 1 are *always* the Node binary and the resolved script path — even when you launch via a symlinked bin. Slice them off to get just the user's arguments: `process.argv.slice(2)`. Index 0 is also available as `process.execPath` and index 1 as `process.argv[1]` / `__filename`.
argv vs execArgv — two different lists

Flags meant for Node itself (the runtime) are split out into process.execArgv and never appear in process.argv. This trips people up constantly:

Bash
node --max-old-space-size=4096 app.js --port 8080

Array

Contents for the command above

process.execArgv

['--max-old-space-size=4096'] — flags consumed by Node

process.argv

[node, app.js, '--port', '8080'] — your program's args

Why the split matters
Node parses *its own* flags before your script runs, so they are removed from the list you see. If you forward args to a child process or re-exec, you often need to replay `process.execArgv` too, or the child loses its memory limit / inspector flags.
The shell parses before Node does
Quoting and globbing happen in the shell
By the time `process.argv` exists, your shell has already split on whitespace, expanded `*` globs, substituted `$VARS`, and stripped one layer of quotes. `node app.js *.txt` may arrive as dozens of filenames; `node app.js "a b"` arrives as the single token `a b`. Node receives the *result*, never the raw line — so you cannot reconstruct the original command from `argv`.

Bash
node app.js "two words" $HOME *.md
# argv[2] = 'two words'  (quotes consumed by the shell)
# argv[3] = '/home/you'  (variable expanded by the shell)
# argv[4..] = each matching .md file (glob expanded by the shell)
Reading positional arguments

JS
const [, , command, target] = process.argv
// node deploy.js build production
// command = 'build', target = 'production'

if (!command) {
  console.error('Usage: node deploy.js <command> <target>')
  process.exit(1)   // non-zero: signal misuse to the shell / CI
}
Parsing flags by hand — and why it gets messy

Hand-rolling works for one or two flags, but notice how many shapes a "flag" can take. Each is a separate branch you must handle:

Form

Example

Meaning

Long boolean

--verbose

presence = true

Long with =

--port=3000

value after the equals sign

Long with space

--port 3000

value is the next token

Short

-p 3000

single-letter alias

Bundled short

-vh

equals -v -h

Terminator

--

everything after is positional, not a flag

JS
const args = process.argv.slice(2)
const verbose = args.includes('--verbose')

// --port=3000  →  read the value after '='
const portArg = args.find((a) => a.startsWith('--port='))
const port = portArg ? Number(portArg.split('=')[1]) : 3000
Built-in parser: util.parseArgs

Since Node 18.3 / 20 the runtime ships a structured parser, so you do not have to reinvent the table above. It handles =, spaces, short aliases, bundling, and the -- terminator for you:

JS
import { parseArgs } from 'node:util'

const { values, positionals } = parseArgs({
  options: {
    port: { type: 'string', short: 'p', default: '3000' },
    verbose: { type: 'boolean', short: 'v' },
  },
  allowPositionals: true,
})

// node app.js serve -p 8080 --verbose
console.log(values)       // { port: '8080', verbose: true }
console.log(positionals)  // [ 'serve' ]
Still strings
`parseArgs` validates *shape*, not *type* — a `'string'` option stays a string. There is no `number` type, so convert and validate yourself (`Number(values.port)`), and reject `NaN` early.
When to reach for a library
  • Small scriptsprocess.argv.slice(2) or util.parseArgs is plenty.

  • Full CLIs — use commander, yargs, or cac for subcommands, auto-generated help, type coercion, and validation. Covered in Parsing CLI Arguments.

Next
Configuration that should not live in the command line — secrets, URLs, run mode — comes from the environment instead. See [Environment Variables (process.env)](/nodejs/process-env).