Skip to content

Build a CLI tool ​

A small wc: it takes file names as arguments, prints line, word and byte counts, and exits with status 1 if a file is missing. It uses process.argv.slice(2), node:fs, node:path, an interface and array methods.

ts
// @args: docs/examples/wc/poem.txt docs/examples/wc/missing.txt docs/examples/wc/notes.txt
// A small `wc`: line, word and byte counts per file, a total, and exit status 1 if any file
// could not be read.
import { existsSync, readFileSync } from "node:fs";
import { basename } from "node:path";

interface Counts {
  lines: number;
  words: number;
  bytes: number;
}

function count(text: string): Counts {
  const words = text
    .split("\n")
    .join(" ")
    .split(" ")
    .filter((w: string): boolean => w !== "");
  return {
    lines: text.split("\n").length - 1,
    words: words.length,
    bytes: text.length, // ASCII input: one byte per character
  };
}

function row(c: Counts, label: string): string {
  const cols = [c.lines, c.words, c.bytes].map((n: number): string => String(n).padStart(6));
  return `${cols.join("")} ${label}`;
}

const files = process.argv.slice(2);
if (files.length === 0) {
  console.log("usage: wc <file>...");
  process.exit(2);
}

const total: Counts = { lines: 0, words: 0, bytes: 0 };
let failed = false;
for (const file of files) {
  if (!existsSync(file)) {
    console.log(`wc: ${basename(file)}: no such file`);
    failed = true;
    continue;
  }
  const c = count(readFileSync(file, "utf8"));
  total.lines += c.lines;
  total.words += c.words;
  total.bytes += c.bytes;
  console.log(row(c, basename(file)));
}
console.log(row(total, "total"));
if (failed) process.exit(1);

Build and run ​

sh
bin/chad build main.ts -o wc
./wc poem.txt missing.txt notes.txt; echo "exit $?"
text
     5     8    41 poem.txt
wc: missing.txt: no such file
     2     7    42 notes.txt
     7    15    83 total
text
exit 1

The docs test suite runs exactly this (with the paths on the program's first line) under Node and as a native binary, and compares stdout and the exit code. The input files:

txt
so much depends
upon

a red wheel
barrow
txt
compile once
run anywhere the binary runs

Things to know ​

  • Only process.argv.slice(2) is accepted (CS1229): Node's argv[0] and argv[1] (the node binary and the script path) have no equivalent in a compiled binary.
  • process.exit(code) sets the exit status; an uncaught error exits with 1, as in Node.
  • The binary starts in about a millisecond, so it is cheap to call from scripts and loops; see Benchmarks.
  • Strings are UTF-8 and exact for ASCII. Counting bytes as text.length is right here because the input is ASCII; non-ASCII string literals are rejected for now (CS1216).

Next: Language reference.