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