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.
Not checked by a trusted editor yet Last edited Outdated?
What this changes
Read-only: it reads the files or standard input and prints word counts; nothing is changed. The optional npm link step makes wordfreq.mjs executable and adds a global wordfreq command.
How to undo it
Nothing to undo for the script. If you ran npm link, remove the global command with npm unlink -g wordfreq.
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.
parseArgstakes a description of the options and returnsvaluesandpositionals. It supports short aliases (-n),--name=value, grouped boolean flags and--to end options. Unknown options throw an error with codeERR_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.isTTYis 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 toconsole.error(stderr). That waywordfreq file > out.txtstays clean. - Exit codes. 0 means success, anything else means failure. Setting
process.exitCodeinstead of callingprocess.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 theuflag), so accented letters such as café count as one word. - Sorting by count and then
localeComparemakes 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,
parseArgsis 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.
- Commitpedia +5,225 chars