CLI#

Node exposes command-line arguments through process.argv. Index 0 is node, index 1 is the script path, and indices 2+ are the user’s arguments. The stdlib ships node:util.parseArgs (Node 18.3+) for basic parsing; for subcommands, type coercion, and --help generation the operator reaches for commander, yargs, or citty.

For the I/O surface scripts read and write through, see I/O. For packaging a script for distribution, see Runtime.

process.argv#

// script.js
console.log("node:", process.argv[0]);
console.log("script:", process.argv[1]);
for (const [i, v] of process.argv.slice(2).entries()) {
  console.log(i, v);
}
$ node script.js -v --port 8080 host
node:   /usr/bin/node
script: /home/op/script.js
0  -v
1  --port
2  8080
3  host

parseArgs (stdlib)#

node:util.parseArgs covers boolean flags, string options, positional arguments. No subcommand support; for that, reach for commander.

import {parseArgs} from "node:util";

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

console.log(values, positionals);
$ node scan.js -v --port 8080 host
{ verbose: true, port: '8080' } [ 'host' ]

Commander#

commander is the default for non-trivial CLIs. Subcommands, type coercion, --help, --version, and a consistent error UX.

$ npm install commander
import {Command} from "commander";

const program = new Command()
  .name("scan")
  .description("Quick port scanner.")
  .version("0.1.0")
  .argument("<host>", "Target host.")
  .option("-p, --port <number>", "Port to probe.", value => Number(value), 80)
  .option("-v, --verbose",       "Verbose output.")
  .action((host, opts) => {
    console.log({host, ...opts});
  });

program.parse();
$ node scan.js --help
Usage: scan [options] <host>
...

Subcommands chain off .command(); each one carries its own options and .action.

program
  .command("up")
  .description("Start the service.")
  .action(start);

program
  .command("down")
  .description("Stop the service.")
  .action(stop);

Exit codes#

process.exit(n) exits with code n. Convention: 0 on success, small positive integer on failure. process.exit fires immediately; pending async writes can be lost. Prefer returning from the entry point and letting the event loop drain naturally; reach for process.exit only when the operator needs to override the exit code.

if (!ok) {
  console.error("error:", err.message);
  process.exit(1);
}

Streaming#

Filter-style scripts read stdin, transform, write stdout. This is the Unix-pipeline shape.

import {createInterface} from "node:readline";

for await (const line of createInterface({input: process.stdin})) {
  process.stdout.write(line.toUpperCase() + "\n");
}
$ echo -e "one\ntwo" | node upper.js
ONE
TWO

Errors and diagnostics go to process.stderr (or console.error) so they do not pollute stdout data.

Shebang#

A #! line on the first line plus chmod +x makes a Node script directly executable.

#!/usr/bin/env node

console.log("hello,", process.argv[2] ?? "world");
$ chmod +x greet.js
$ ./greet.js operator
hello, operator

For a script you want on PATH system-wide, declare it as a bin entry in package.json.

{
  "name": "myscan",
  "bin": {"scan": "./bin/scan.js"}
}
$ npm link              # symlink myscan/bin/scan.js into $PATH

References#