Command-line arguments and user input

Node.js · Essentials · 11 min

Small CLI tools are one of the most common things people write in Node.

process.argv gives raw strings. For real flags use the built-in `util.parseArgs`, which handles --name value, --name=value, short aliases and booleans:

  • Declare options with a type ("string" or "boolean"), optional short, default and multiple.
  • Set allowPositionals: true to accept bare arguments.
  • Unknown flags throw by default (strict: true), so typos don't pass silently.

For interactive questions, use `node:readline/promises`. Create an interface over stdin/stdout, await rl.question("Name? "), and rl.close() when done — an open interface keeps the process alive.

Tools that work well in pipelines also read stdin: cat data.txt | node count.js. process.stdin is a readable stream, so you can iterate it with for await, or use readline.createInterface({ input: process.stdin }) and for await (const line of rl) to get one line at a time. process.stdin.isTTY tells you whether a human is typing or data is piped in.

For output, keep it simple: console.log for results, console.error for messages. util.styleText("red", "error") adds colour without a dependency and is ignored automatically when output isn't a terminal in recent Node versions; console.table prints arrays of objects nicely.

When a CLI grows subcommands, help text and completion, a library like commander is reasonable, but many tools never need more than parseArgs.

Example

// grep-lite.js — usage: node grep-lite.js --ignore-case error < app.log
import { parseArgs, styleText } from "node:util";
import { createInterface } from "node:readline/promises";

const { values, positionals } = parseArgs({
  options: {
    "ignore-case": { type: "boolean", short: "i", default: false },
    max: { type: "string", default: "100" },
  },
  allowPositionals: true,
});

const [pattern] = positionals;
if (!pattern) {
  console.error("usage: grep-lite [-i] [--max N] <pattern>");
  process.exit(2);
}
const re = new RegExp(pattern, values["ignore-case"] ? "i" : "");
let hits = 0;

const rl = createInterface({ input: process.stdin });
for await (const line of rl) {
  if (re.test(line) && hits++ < Number(values.max)) console.log(styleText("yellow", line));
}

Remember

  • util.parseArgs handles flags, aliases and positionals without dependencies
  • readline/promises for questions; close the interface when finished
  • for await over a readline interface reads piped stdin line by line

Watch out for

  • Forgetting rl.close(), so the process hangs waiting for input.

Check yourself

  1. With parseArgs in its default strict mode, what happens when the user passes an undeclared --verbose flag?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/util.html#utilparseargsconfig

Builds on

Unlocks

Nothing builds on this yet.