Skip to content

Commit 6ac53b9

Browse files
committed
wc: support multiple files and print grand total (no flags)
1 parent bc06d2e commit 6ac53b9

File tree

1 file changed

+54
-32
lines changed
  • implement-shell-tools/wc

1 file changed

+54
-32
lines changed

implement-shell-tools/wc/wc.js

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,75 @@
11
#!/usr/bin/env node
2-
// wc.js — ESM: single file, no flags
3-
// Prints: lines words bytes filename
2+
// wc.js — ESM: multiple files + total (no flags)
3+
// Prints per-file: lines words bytes filename
4+
// If given >1 files, also prints a "total" line.
45

56
import fs from "node:fs";
67
import { pathToFileURL } from "node:url";
78

89
async function main() {
9-
const args = process.argv.slice(2);
10-
11-
// This commit supports exactly ONE file (no flags yet)
12-
if (args.length !== 1) {
13-
console.error("Usage (this commit): node wc.js <single-file>");
10+
const files = process.argv.slice(2);
11+
if (files.length === 0) {
12+
console.error("Usage (this commit): node wc.js <file...>");
1413
process.exit(1);
1514
}
1615

17-
const file = args[0];
18-
19-
try {
20-
const buf = await fs.promises.readFile(file); // Buffer
21-
const bytes = buf.length;
16+
let hadError = false;
17+
let totalLines = 0, totalWords = 0, totalBytes = 0;
18+
const results = [];
2219

23-
// Count lines: number of newline characters '\n'
24-
let lines = 0;
25-
for (let i = 0; i < buf.length; i++) {
26-
if (buf[i] === 0x0a) lines++; // '\n'
20+
for (const file of files) {
21+
try {
22+
const st = await fs.promises.lstat(file);
23+
if (st.isDirectory()) {
24+
console.error(`wc: ${file}: Is a directory`);
25+
hadError = true;
26+
continue;
27+
}
28+
const { lines, words, bytes } = await countFile(file);
29+
results.push({ file, lines, words, bytes });
30+
totalLines += lines; totalWords += words; totalBytes += bytes;
31+
} catch (err) {
32+
if (err?.code === "ENOENT") {
33+
console.error(`wc: ${file}: No such file or directory`);
34+
} else if (err?.code === "EACCES") {
35+
console.error(`wc: ${file}: Permission denied`);
36+
} else {
37+
console.error(`wc: ${file}: ${err?.message || "Error"}`);
38+
}
39+
hadError = true;
2740
}
41+
}
2842

29-
// Count words: sequences of non-whitespace
30-
const text = buf.toString("utf8");
31-
const words = (text.match(/\S+/g) || []).length;
32-
33-
console.log(`${pad(lines)} ${pad(words)} ${pad(bytes)} ${file}`);
34-
} catch (err) {
35-
if (err?.code === "ENOENT") {
36-
console.error(`wc: ${file}: No such file or directory`);
37-
} else if (err?.code === "EACCES") {
38-
console.error(`wc: ${file}: Permission denied`);
39-
} else {
40-
console.error(`wc: ${file}: ${err?.message || "Error"}`);
41-
}
42-
process.exitCode = 1;
43+
for (const r of results) {
44+
console.log(`${pad(r.lines)} ${pad(r.words)} ${pad(r.bytes)} ${r.file}`);
45+
}
46+
if (results.length > 1) {
47+
console.log(`${pad(totalLines)} ${pad(totalWords)} ${pad(totalBytes)} total`);
4348
}
49+
50+
if (hadError) process.exitCode = 1;
51+
}
52+
53+
async function countFile(file) {
54+
const buf = await fs.promises.readFile(file); // Buffer
55+
const bytes = buf.length;
56+
57+
// lines: count '\n' bytes
58+
let lines = 0;
59+
for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) lines++;
60+
61+
// words: sequences of non-whitespace (on UTF-8 text)
62+
const text = buf.toString("utf8");
63+
const words = (text.match(/\S+/g) || []).length;
64+
65+
return { lines, words, bytes };
4466
}
4567

4668
function pad(n) {
47-
// Right-align like wc (fixed width helps match spacing)
69+
// Right-align like `wc` (fixed width works well for visual parity)
4870
return String(n).padStart(7, " ");
4971
}
5072

51-
// Run only when executed directly
73+
// run only when executed directly
5274
const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href;
5375
if (isDirect) await main();

0 commit comments

Comments
 (0)