Skip to content
Commitpedia
Log in Join & edit

Build a small Node.js CLI with util.parseArgs

A complete, dependency-free command-line tool in Node: options and flags with parseArgs, input from files or a pipe, help text, and meaningful exit codes.

Edit history of Build a small Node.js CLI with util.parseArgs: 1 recorded edit
Edit history · 1 edit
Type
Script
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Language
javascript
Licence
CC0-1.0

You do not need a framework to write a useful command-line tool. Node has a built-in argument parser, util.parseArgs, and everything else is standard input, standard output and exit codes. This example counts the most frequent words in text.

The script

Save as wordfreq.mjs. Works on Node 20 and later with no dependencies.

#!/usr/bin/env node
import { parseArgs } from "node:util";
import { readFile } from "node:fs/promises";
import { text } from "node:stream/consumers";

const HELP = `Usage: wordfreq [options] [file...]

Count the most frequent words in files or standard input.

Options:
  -n, --top <number>      how many words to show (default 10)
  -m, --min-length <n>    ignore words shorter than n (default 1)
  -j, --json              print JSON instead of a table
  -h, --help              show this help`;

function parseCli(argv) {
  const { values, positionals } = parseArgs({
    args: argv,
    allowPositionals: true,
    options: {
      top: { type: "string", short: "n", default: "10" },
      "min-length": { type: "string", short: "m", default: "1" },
      json: { type: "boolean", short: "j", default: false },
      help: { type: "boolean", short: "h", default: false },
    },
  });
  const top = Number.parseInt(values.top, 10);
  const minLength = Number.parseInt(values["min-length"], 10);
  if (!Number.isInteger(top) || top < 1) throw new Error("--top must be a positive integer");
  if (!Number.isInteger(minLength) || minLength < 1) throw new Error("--min-length must be a positive integer");
  return { files: positionals, top, minLength, json: values.json, help: values.help };
}

async function readInput(files) {
  if (files.length === 0) {
    if (process.stdin.isTTY) throw new Error("no input: pass a file or pipe text in");
    return text(process.stdin);
  }
  const parts = await Promise.all(files.map((f) => readFile(f, "utf8")));
  return parts.join("\n");
}

function countWords(input, minLength) {
  const counts = new Map();
  for (const [word] of input.toLowerCase().matchAll(/\p{L}[\p{L}\p{N}'-]*/gu)) {
    if (word.length < minLength) continue;
    counts.set(word, (counts.get(word) ?? 0) + 1);
  }
  return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}

async function main() {
  const options = parseCli(process.argv.slice(2));
  if (options.help) {
    console.log(HELP);
    return;
  }
  const ranked = countWords(await readInput(options.files), options.minLength).slice(0, options.top);
  if (options.json) {
    console.log(JSON.stringify(Object.fromEntries(ranked), null, 2));
    return;
  }
  const width = Math.max(4, ...ranked.map(([w]) => w.length));
  for (const [word, count] of ranked) console.log(`${word.padEnd(width)}  ${count}`);
}

main().catch((err) => {
  console.error(`wordfreq: ${err.message}`);
  if (err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") console.error("Try --help.");
  process.exitCode = err.code === "ENOENT" ? 2 : 1;
});

Try it

node wordfreq.mjs README.md
node wordfreq.mjs --top 5 --min-length 4 notes/*.md
cat log.txt | node wordfreq.mjs -n 3 --json
node wordfreq.mjs --help

The parts every CLI needs

  • Arguments. parseArgs takes a description of the options and returns values and positionals. It supports short aliases (-n), --name=value, grouped boolean flags and -- to end options. Unknown options throw an error with code ERR_PARSE_ARGS_UNKNOWN_OPTION.
  • Validation. Option values arrive as strings. Convert and check them early, and fail with a clear message.
  • Input. Read named files, or fall back to standard input so the tool works in pipes. process.stdin.isTTY is true when nothing is piped in, which lets you print a hint instead of waiting forever.
  • Output. Results go to console.log (stdout); messages and errors go to console.error (stderr). That way wordfreq file > out.txt stays clean.
  • Exit codes. 0 means success, anything else means failure. Setting process.exitCode instead of calling process.exit() lets pending output flush before Node exits.

Make it a real command

The first line, #!/usr/bin/env node, lets macOS and Linux run the file directly. To install it as a command, add a bin entry to package.json and link it:

{
  "name": "wordfreq",
  "version": "1.0.0",
  "type": "module",
  "bin": { "wordfreq": "./wordfreq.mjs" }
}
chmod +x wordfreq.mjs
npm link
wordfreq --top 3 README.md

npm link creates a global symlink to your folder, so edits take effect immediately. Run npm unlink -g wordfreq to remove it.

Notes

  • The word pattern uses Unicode property escapes (\p{L} with the u flag), so accented letters such as café count as one word.
  • Sorting by count and then localeCompare makes the output stable when counts tie.
  • When the tool grows subcommands, colored output or prompts, libraries like Commander or yargs save time; for a handful of options, parseArgs is enough.

See also

  • 0021 Read and write files in Node.js with fs/promises A complete Node script that walks a folder, reads text files, and writes a JSON report safely, showing the everyday fs/promises calls… Beginner · How-to · Any system Script Node.js 15 Oct 2026
  • 0027 Fix: SyntaxError: Cannot use import statement outside a module The file uses ES module import syntax, but Node or the browser is loading it as a classic script or CommonJS. Tell… Intermediate · Fix · Any system Article Common errors · Node.js 15 Oct 2026
  • 0263 Build a notes REST API with plain Node.js: CRUD, validation, status codes and CORS Write a small JSON API for notes using only Node.js built-ins: create, read, update and delete, input validation, correct HTTP status codes,… Intermediate · Tutorial · Any system Article Making requests · Node.js · Status codes 15 Oct 2026

Repos in the wild

  • Node.js nodejs/node The source of the Node.js runtime: a JavaScript engine (V8) plus APIs for files, networking, processes and more, used to run JavaScript outside the browser. GitHub
  • Express (expressjs/express) expressjs/express A minimal and flexible web framework for Node.js, built around routes and middleware, and the foundation of countless APIs and other frameworks. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. commitpedia-admin +5,225 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?