Word Count CLI in Node.js
A small command-line tool that reports lines, words, and UTF-8 bytes for a file, with unit and CLI tests that verify output and error handling.
Project
node-word-count-cli
Entry point: cli.mjs
Install
node --versionRun
node cli.mjs sample.txtTest
node --test cli.test.mjsExpected output
Prints JSON such as {"lines":2,"words":3,"bytes":14} and exits with status 1 when the file argument is missing.Dependencies
- node >=20
Verification evidence
Node 24.15: node --test -> 2 passed; CLI printed {"lines":2,"words":3,"bytes":14} for a sample file and exited 1 without arguments.
Security note
Reads only the path provided by the caller; do not run it against untrusted huge files without streaming or size limits.
What it does
Counts lines, whitespace-separated words, and UTF-8 bytes for a file. The counting function is exported so it can be unit-tested, and the CLI prints JSON.
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
export function countText(text) {
const lines = text.split('\n').length;
const words = (text.match(/\S+/g) ?? []).length;
return { lines, words, bytes: Buffer.byteLength(text, 'utf8') };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const file = process.argv[2];
if (!file) {
console.error('usage: node cli.mjs <file>');
process.exit(1);
}
console.log(JSON.stringify(countText(readFileSync(file, 'utf8'))));
}
Test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { countText } from './cli.mjs';
test('counts lines, words and bytes', () => {
assert.deepEqual(countText('hello world\nsecond line\n'), { lines: 3, words: 4, bytes: 24 });
});
test('CLI prints JSON and rejects missing file', () => {
const dir = mkdtempSync(join(tmpdir(), 'wc-'));
const file = join(dir, 'sample.txt');
writeFileSync(file, 'one two three\n');
try {
const ok = spawnSync(process.execPath, ['cli.mjs', file], { encoding: 'utf8' });
assert.equal(ok.status, 0);
assert.deepEqual(JSON.parse(ok.stdout), { lines: 2, words: 3, bytes: 14 });
const bad = spawnSync(process.execPath, ['cli.mjs'], { encoding: 'utf8' });
assert.equal(bad.status, 1);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
Run and verify
node --test cli.test.mjs
printf 'one two three\n' > sample.txt
node cli.mjs sample.txt
Expected output: a JSON object such as {"lines":2,"words":3,"bytes":14}. Missing arguments print usage and exit with status 1. Verified with Node 24.15: both tests passed.
Limitations
Loads the whole file into memory and treats a trailing newline as an extra line segment. For very large files, stream chunks and define line counting semantics explicitly.