← Runnable Projects

阅读中文版本

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.

beginnerjavascriptverified-runnable

Project

node-word-count-cli

Entry point: cli.mjs

Install

node --version

Run

node cli.mjs sample.txt

Test

node --test cli.test.mjs

Expected 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.