From 1a250cb414b3cc936ad54e231e660ad20a23731d Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:25:27 +0100 Subject: [PATCH 01/11] cat: scaffold command (placeholder, no behavior yet) --- implement-shell-tools/cat/cat.js | 17 +++++++++++++++++ implement-shell-tools/package.json | 12 ++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 implement-shell-tools/cat/cat.js create mode 100644 implement-shell-tools/package.json diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 00000000..54a57404 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node +// cat.js — scaffold (no functionality yet) +// Next commits will add: +// - basic file output +// - -n (number all lines) +// - -b (number non-blank) + +function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error("Usage: node cat.js "); + process.exit(1); + } + console.log("cat: scaffold ready (implementation comes in next commit)"); +} + +if (require.main === module) main(); diff --git a/implement-shell-tools/package.json b/implement-shell-tools/package.json new file mode 100644 index 00000000..8740af3e --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,12 @@ +{ + "type": "module", + "name": "implement-shell-tools-with-node", + "version": "1.0.0", + "description": "Your task is to re-implement shell tools you have used.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "pezhman azizi", + "license": "ISC" +} From 59bc08347b02808064ffb99e4dc78a5b5c9e027c Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:54:48 +0100 Subject: [PATCH 02/11] cat: implement basic file concatenation (no flags) --- implement-shell-tools/cat/cat.js | 54 ++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 54a57404..c91441a8 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -1,17 +1,45 @@ #!/usr/bin/env node -// cat.js — scaffold (no functionality yet) -// Next commits will add: -// - basic file output -// - -n (number all lines) -// - -b (number non-blank) +// cat.js — basic version (no flags) -function main() { - const args = process.argv.slice(2); - if (args.length === 0) { - console.error("Usage: node cat.js "); - process.exit(1); - } - console.log("cat: scaffold ready (implementation comes in next commit)"); +import fs from 'fs'; + +const files = process.argv.slice(2); + +if (files.length === 0) { + console.error("Usage: node cat.js "); + process.exit(1); } -if (require.main === module) main(); +let hadError = false; + +(async () => { + for (const file of files) { + try { + const stat = await fs.promises.stat(file); + if (stat.isDirectory()) { + console.error(`cat: ${file}: Is a directory`); + hadError = true; + continue; + } + await pipeFile(file); + } catch (err) { + if (err?.code === "ENOENT" || err?.code === "ENOTDIR") { + console.error(`cat: ${file}: No such file or directory`); + } else { + console.error(`cat: ${file}: ${err?.message || "Error"}`); + } + hadError = true; + } + } + if (hadError) process.exitCode = 1; +})(); + +function pipeFile(file) { + return new Promise((resolve, reject) => { + const rs = fs.createReadStream(file); + rs.on("error", reject); + rs.on("end", resolve); + // keep stdout open between files + rs.pipe(process.stdout, { end: false }); + }); +} From 4e01f5a05ca5bad10859594073cd780261b9faf0 Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:59:12 +0100 Subject: [PATCH 03/11] cat(ESM): add -n to number all lines (continues across files) --- implement-shell-tools/cat/cat.js | 76 +++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index c91441a8..38acac10 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -1,38 +1,51 @@ #!/usr/bin/env node -// cat.js — basic version (no flags) +// cat.js — ESM version with -n (number all lines) -import fs from 'fs'; +import fs from "node:fs"; +import readline from "node:readline"; -const files = process.argv.slice(2); +const args = process.argv.slice(2); +let numberAll = false; +const files = []; + +// parse flags + files +for (const a of args) { + if (a === "-n") numberAll = true; + else files.push(a); +} if (files.length === 0) { - console.error("Usage: node cat.js "); + console.error("Usage: node cat.js [-n] "); process.exit(1); } let hadError = false; +let lineNo = 1; -(async () => { - for (const file of files) { - try { - const stat = await fs.promises.stat(file); - if (stat.isDirectory()) { - console.error(`cat: ${file}: Is a directory`); - hadError = true; - continue; - } - await pipeFile(file); - } catch (err) { - if (err?.code === "ENOENT" || err?.code === "ENOTDIR") { - console.error(`cat: ${file}: No such file or directory`); - } else { - console.error(`cat: ${file}: ${err?.message || "Error"}`); - } +for (const file of files) { + try { + const stat = await fs.promises.stat(file); + if (stat.isDirectory()) { + console.error(`cat: ${file}: Is a directory`); hadError = true; + continue; + } + if (numberAll) { + await numberFile(file); + } else { + await pipeFile(file); } + } catch (err) { + if (err?.code === "ENOENT" || err?.code === "ENOTDIR") { + console.error(`cat: ${file}: No such file or directory`); + } else { + console.error(`cat: ${file}: ${err?.message || "Error"}`); + } + hadError = true; } - if (hadError) process.exitCode = 1; -})(); +} + +if (hadError) process.exitCode = 1; function pipeFile(file) { return new Promise((resolve, reject) => { @@ -43,3 +56,22 @@ function pipeFile(file) { rs.pipe(process.stdout, { end: false }); }); } + +function numberFile(file) { + return new Promise((resolve, reject) => { + const rs = fs.createReadStream(file); + const rl = readline.createInterface({ input: rs, crlfDelay: Infinity }); + + rl.on("line", (line) => { + process.stdout.write(formatNum(lineNo++) + line + "\n"); + }); + rl.on("close", resolve); + rl.on("error", reject); + rs.on("error", reject); + }); +} + +function formatNum(n) { + // match GNU cat -n: 6-wide, right-aligned, then a tab + return String(n).padStart(6, " ") + "\t"; +} From d5eafa1e0ed0e3a92fe8743bdb0768278ca4373a Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:02:27 +0100 Subject: [PATCH 04/11] cat(ESM): add -b to number non-blank lines; -b overrides -n --- implement-shell-tools/cat/cat.js | 33 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 38acac10..d359e331 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -1,21 +1,25 @@ #!/usr/bin/env node -// cat.js — ESM version with -n (number all lines) +// cat.js — ESM: supports -n and -b (-b overrides -n) import fs from "node:fs"; import readline from "node:readline"; const args = process.argv.slice(2); let numberAll = false; +let numberNonblank = false; const files = []; -// parse flags + files +// parse flags + files (flags can appear anywhere) for (const a of args) { if (a === "-n") numberAll = true; + else if (a === "-b") numberNonblank = true; else files.push(a); } +// precedence: -b wins over -n +if (numberNonblank) numberAll = false; if (files.length === 0) { - console.error("Usage: node cat.js [-n] "); + console.error("Usage: node cat.js [-n|-b] "); process.exit(1); } @@ -30,10 +34,10 @@ for (const file of files) { hadError = true; continue; } - if (numberAll) { - await numberFile(file); - } else { + if (!numberAll && !numberNonblank) { await pipeFile(file); + } else { + await numberFile(file, { nonblank: numberNonblank }); } } catch (err) { if (err?.code === "ENOENT" || err?.code === "ENOTDIR") { @@ -52,18 +56,25 @@ function pipeFile(file) { const rs = fs.createReadStream(file); rs.on("error", reject); rs.on("end", resolve); - // keep stdout open between files - rs.pipe(process.stdout, { end: false }); + rs.pipe(process.stdout, { end: false }); // keep stdout open between files }); } -function numberFile(file) { +function numberFile(file, { nonblank }) { return new Promise((resolve, reject) => { const rs = fs.createReadStream(file); const rl = readline.createInterface({ input: rs, crlfDelay: Infinity }); rl.on("line", (line) => { - process.stdout.write(formatNum(lineNo++) + line + "\n"); + if (nonblank) { + if (line.length === 0) { + process.stdout.write("\n"); // blank line, no number + } else { + process.stdout.write(formatNum(lineNo++) + line + "\n"); + } + } else { + process.stdout.write(formatNum(lineNo++) + line + "\n"); + } }); rl.on("close", resolve); rl.on("error", reject); @@ -72,6 +83,6 @@ function numberFile(file) { } function formatNum(n) { - // match GNU cat -n: 6-wide, right-aligned, then a tab + // GNU cat style: width 6, right-aligned, then a tab return String(n).padStart(6, " ") + "\t"; } From fff19283129a88600d97d46831814b0e580c7c42 Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:18:53 +0100 Subject: [PATCH 05/11] ls: scaffold command (placeholder, no behavior yet) --- implement-shell-tools/ls/ls.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 implement-shell-tools/ls/ls.js diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 00000000..878fc606 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node +// ls.js — scaffold (no functionality yet) +// Next commits will add: +// - basic listing of '.' or given paths (no flags) +// - -a (include dotfiles) +// - -1 (one entry per line; simple output format) +// - -l (long format) [stretch if required] + +import { pathToFileURL } from "node:url"; + +function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error("Usage: node ls.js [path...]"); + process.exit(1); + } + console.log("ls: scaffold ready (implementation comes in next commit)"); +} + +// run only when executed directly +const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; +if (isDirect) main(); From f495a40feddb1f3ecac83786018a6be4953f9ff9 Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:28:13 +0100 Subject: [PATCH 06/11] =?UTF-8?q?ls:=20basic=20listing=20(no=20flags)=20?= =?UTF-8?q?=E2=80=94=20excludes=20dotfiles,=20one-per-line,=20multi-path?= =?UTF-8?q?=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- implement-shell-tools/ls/ls.js | 60 ++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index 878fc606..a42e100b 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -1,22 +1,56 @@ #!/usr/bin/env node -// ls.js — scaffold (no functionality yet) -// Next commits will add: -// - basic listing of '.' or given paths (no flags) -// - -a (include dotfiles) -// - -1 (one entry per line; simple output format) -// - -l (long format) [stretch if required] +// ls.js — ESM basic version (no flags) +// - If no paths: list '.' +// - Excludes dotfiles by default +// - One entry per line, alphabetical +// - Multiple paths: print "path:" header before each directory group +// - GNU-like errors; exit code 1 on any failure +import fs from "node:fs"; import { pathToFileURL } from "node:url"; -function main() { - const args = process.argv.slice(2); - if (args.length === 0) { - console.error("Usage: node ls.js [path...]"); - process.exit(1); +const args = process.argv.slice(2); +const targets = args.length ? args : ["."]; +let hadError = false; + +for (let i = 0; i < targets.length; i++) { + const target = targets[i]; + + try { + const stat = await fs.promises.lstat(target); + + if (stat.isDirectory()) { + if (targets.length > 1) console.log(`${target}:`); + + const entries = await fs.promises.readdir(target, { withFileTypes: true }); + const names = entries + .map(d => d.name) + .filter(n => !n.startsWith(".")) // no dotfiles (we’ll add -a later) + .sort((a, b) => a.localeCompare(b)); + + for (const name of names) console.log(name); + + if (targets.length > 1 && i !== targets.length - 1) console.log(""); // blank line between dir groups + } else { + // plain file or symlink -> print the argument as given + console.log(target); + } + } catch (err) { + if (err?.code === "ENOENT") { + console.error(`ls: cannot access '${target}': No such file or directory`); + } else if (err?.code === "EACCES") { + console.error(`ls: cannot open directory '${target}': Permission denied`); + } else { + console.error(`ls: ${target}: ${err?.message || "Error"}`); + } + hadError = true; } - console.log("ls: scaffold ready (implementation comes in next commit)"); } +if (hadError) process.exitCode = 1; + // run only when executed directly const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; -if (isDirect) main(); +if (!isDirect) { + // allow import in tests without auto-running +} From cf8040c84f94703173092d5fd6267db13f6305ea Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 11:30:46 +0100 Subject: [PATCH 07/11] ls: add -a (include dotfiles) and accept -1 (one-per-line) --- implement-shell-tools/ls/ls.js | 80 ++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index a42e100b..b5ba856a 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -1,47 +1,73 @@ #!/usr/bin/env node -// ls.js — ESM basic version (no flags) -// - If no paths: list '.' -// - Excludes dotfiles by default -// - One entry per line, alphabetical -// - Multiple paths: print "path:" header before each directory group -// - GNU-like errors; exit code 1 on any failure + import fs from "node:fs"; import { pathToFileURL } from "node:url"; -const args = process.argv.slice(2); -const targets = args.length ? args : ["."]; -let hadError = false; +const raw = process.argv.slice(2); -for (let i = 0; i < targets.length; i++) { - const target = targets[i]; +let includeAll = false; // -a +let onePerLine = false; // -1 (format already one-per-line) +const targets = []; +// Parse flags (supports combined like -a1 / -1a). Treat lone "-" as a path. +for (const arg of raw) { + if (arg === "-" || !arg.startsWith("-")) { + targets.push(arg); + continue; + } + if (arg === "-1") { + onePerLine = true; + continue; + } + for (const ch of arg.slice(1)) { + if (ch === "a") includeAll = true; + else if (ch === "1") onePerLine = true; + else { + // ignore unknown short flags + } + } +} + +const paths = targets.length ? targets : ["."]; +let hadError = false; + +for (let i = 0; i < paths.length; i++) { + const p = paths[i]; try { - const stat = await fs.promises.lstat(target); + const st = await fs.promises.lstat(p); + + if (st.isDirectory()) { + if (paths.length > 1) console.log(`${p}:`); - if (stat.isDirectory()) { - if (targets.length > 1) console.log(`${target}:`); + const entries = await fs.promises.readdir(p, { withFileTypes: true }); + let names = entries.map(d => d.name); - const entries = await fs.promises.readdir(target, { withFileTypes: true }); - const names = entries - .map(d => d.name) - .filter(n => !n.startsWith(".")) // no dotfiles (we’ll add -a later) - .sort((a, b) => a.localeCompare(b)); + if (!includeAll) { + names = names.filter(n => !n.startsWith(".")); + } else { + // mimic `ls -a` by including "." and ".." + names = ["." , "..", ...names]; + } - for (const name of names) console.log(name); + names.sort((a, b) => a.localeCompare(b)); + for (const name of names) { + // one-per-line output; -1 flag just affirms it + console.log(name); + } - if (targets.length > 1 && i !== targets.length - 1) console.log(""); // blank line between dir groups + if (paths.length > 1 && i !== paths.length - 1) console.log(""); } else { - // plain file or symlink -> print the argument as given - console.log(target); + // file or symlink => print the argument as given + console.log(p); } } catch (err) { if (err?.code === "ENOENT") { - console.error(`ls: cannot access '${target}': No such file or directory`); + console.error(`ls: cannot access '${p}': No such file or directory`); } else if (err?.code === "EACCES") { - console.error(`ls: cannot open directory '${target}': Permission denied`); + console.error(`ls: cannot open directory '${p}': Permission denied`); } else { - console.error(`ls: ${target}: ${err?.message || "Error"}`); + console.error(`ls: ${p}: ${err?.message || "Error"}`); } hadError = true; } @@ -52,5 +78,5 @@ if (hadError) process.exitCode = 1; // run only when executed directly const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; if (!isDirect) { - // allow import in tests without auto-running + // allow importing in tests without auto-executing } From bf61510925f7f725ef1b8068f80e0e031372ef94 Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:05:55 +0100 Subject: [PATCH 08/11] wc: scaffold command (placeholder, no behavior yet) --- implement-shell-tools/wc/wc.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 implement-shell-tools/wc/wc.js diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 00000000..5f108551 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node +// wc.js — scaffold (no functionality yet) +// Plan: +// 1) single-file, no flags: print lines words bytes + filename +// 2) multiple files: print per-file + "total" +// 3) flags: -l (lines), -w (words), -c (bytes) + +import { pathToFileURL } from "node:url"; + +function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error("Usage: node wc.js | [-l|-w|-c] "); + process.exit(1); + } + console.log("wc: scaffold ready (implementation comes in next commit)"); +} + +// run only when executed directly +const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; +if (isDirect) main(); From bc06d2eb24589bd82ca3a81c7329d4cbe8b445dd Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:16:47 +0100 Subject: [PATCH 09/11] =?UTF-8?q?wc:=20implement=20single-file=20counts=20?= =?UTF-8?q?(lines,=20words,=20bytes)=20=E2=80=94=20no=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- implement-shell-tools/wc/wc.js | 54 +++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index 5f108551..913cd637 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -1,21 +1,53 @@ #!/usr/bin/env node -// wc.js — scaffold (no functionality yet) -// Plan: -// 1) single-file, no flags: print lines words bytes + filename -// 2) multiple files: print per-file + "total" -// 3) flags: -l (lines), -w (words), -c (bytes) +// wc.js — ESM: single file, no flags +// Prints: lines words bytes filename +import fs from "node:fs"; import { pathToFileURL } from "node:url"; -function main() { +async function main() { const args = process.argv.slice(2); - if (args.length === 0) { - console.error("Usage: node wc.js | [-l|-w|-c] "); + + // This commit supports exactly ONE file (no flags yet) + if (args.length !== 1) { + console.error("Usage (this commit): node wc.js "); process.exit(1); } - console.log("wc: scaffold ready (implementation comes in next commit)"); + + const file = args[0]; + + try { + const buf = await fs.promises.readFile(file); // Buffer + const bytes = buf.length; + + // Count lines: number of newline characters '\n' + let lines = 0; + for (let i = 0; i < buf.length; i++) { + if (buf[i] === 0x0a) lines++; // '\n' + } + + // Count words: sequences of non-whitespace + const text = buf.toString("utf8"); + const words = (text.match(/\S+/g) || []).length; + + console.log(`${pad(lines)} ${pad(words)} ${pad(bytes)} ${file}`); + } catch (err) { + if (err?.code === "ENOENT") { + console.error(`wc: ${file}: No such file or directory`); + } else if (err?.code === "EACCES") { + console.error(`wc: ${file}: Permission denied`); + } else { + console.error(`wc: ${file}: ${err?.message || "Error"}`); + } + process.exitCode = 1; + } +} + +function pad(n) { + // Right-align like wc (fixed width helps match spacing) + return String(n).padStart(7, " "); } -// run only when executed directly +// Run only when executed directly const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; -if (isDirect) main(); +if (isDirect) await main(); From 6ac53b923407a63b81d1dc1d514def4766d94be8 Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:21:18 +0100 Subject: [PATCH 10/11] wc: support multiple files and print grand total (no flags) --- implement-shell-tools/wc/wc.js | 86 +++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index 913cd637..cc878dc1 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -1,53 +1,75 @@ #!/usr/bin/env node -// wc.js — ESM: single file, no flags -// Prints: lines words bytes filename +// wc.js — ESM: multiple files + total (no flags) +// Prints per-file: lines words bytes filename +// If given >1 files, also prints a "total" line. import fs from "node:fs"; import { pathToFileURL } from "node:url"; async function main() { - const args = process.argv.slice(2); - - // This commit supports exactly ONE file (no flags yet) - if (args.length !== 1) { - console.error("Usage (this commit): node wc.js "); + const files = process.argv.slice(2); + if (files.length === 0) { + console.error("Usage (this commit): node wc.js "); process.exit(1); } - const file = args[0]; - - try { - const buf = await fs.promises.readFile(file); // Buffer - const bytes = buf.length; + let hadError = false; + let totalLines = 0, totalWords = 0, totalBytes = 0; + const results = []; - // Count lines: number of newline characters '\n' - let lines = 0; - for (let i = 0; i < buf.length; i++) { - if (buf[i] === 0x0a) lines++; // '\n' + for (const file of files) { + try { + const st = await fs.promises.lstat(file); + if (st.isDirectory()) { + console.error(`wc: ${file}: Is a directory`); + hadError = true; + continue; + } + const { lines, words, bytes } = await countFile(file); + results.push({ file, lines, words, bytes }); + totalLines += lines; totalWords += words; totalBytes += bytes; + } catch (err) { + if (err?.code === "ENOENT") { + console.error(`wc: ${file}: No such file or directory`); + } else if (err?.code === "EACCES") { + console.error(`wc: ${file}: Permission denied`); + } else { + console.error(`wc: ${file}: ${err?.message || "Error"}`); + } + hadError = true; } + } - // Count words: sequences of non-whitespace - const text = buf.toString("utf8"); - const words = (text.match(/\S+/g) || []).length; - - console.log(`${pad(lines)} ${pad(words)} ${pad(bytes)} ${file}`); - } catch (err) { - if (err?.code === "ENOENT") { - console.error(`wc: ${file}: No such file or directory`); - } else if (err?.code === "EACCES") { - console.error(`wc: ${file}: Permission denied`); - } else { - console.error(`wc: ${file}: ${err?.message || "Error"}`); - } - process.exitCode = 1; + for (const r of results) { + console.log(`${pad(r.lines)} ${pad(r.words)} ${pad(r.bytes)} ${r.file}`); + } + if (results.length > 1) { + console.log(`${pad(totalLines)} ${pad(totalWords)} ${pad(totalBytes)} total`); } + + if (hadError) process.exitCode = 1; +} + +async function countFile(file) { + const buf = await fs.promises.readFile(file); // Buffer + const bytes = buf.length; + + // lines: count '\n' bytes + let lines = 0; + for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) lines++; + + // words: sequences of non-whitespace (on UTF-8 text) + const text = buf.toString("utf8"); + const words = (text.match(/\S+/g) || []).length; + + return { lines, words, bytes }; } function pad(n) { - // Right-align like wc (fixed width helps match spacing) + // Right-align like `wc` (fixed width works well for visual parity) return String(n).padStart(7, " "); } -// Run only when executed directly +// run only when executed directly const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; if (isDirect) await main(); From 4d73eab602f8a62aafdbd6c1f2d56e98c5df4651 Mon Sep 17 00:00:00 2001 From: Pezhman-Azizi <80008463+Pezhman-Azizi@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:22:28 +0100 Subject: [PATCH 11/11] wc: add flags -l (lines), -w (words), -c (bytes) with multi-file totals --- implement-shell-tools/wc/wc.js | 68 +++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index cc878dc1..5067e0d2 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -1,20 +1,50 @@ #!/usr/bin/env node -// wc.js — ESM: multiple files + total (no flags) -// Prints per-file: lines words bytes filename -// If given >1 files, also prints a "total" line. +// wc.js — ESM: supports flags -l (lines), -w (words), -c (bytes) +// Behaviors covered by the README: +// - wc sample-files/* +// - wc -l sample-files/3.txt +// - wc -w sample-files/3.txt +// - wc -c sample-files/3.txt +// - wc -l sample-files/* +// Also works with multiple flags (e.g., -lw), like GNU wc. import fs from "node:fs"; import { pathToFileURL } from "node:url"; async function main() { - const files = process.argv.slice(2); + const argv = process.argv.slice(2); + + let showLines = false; + let showWords = false; + let showBytes = false; + const files = []; + + // Parse flags + files. Support combined short flags like -lw, -cl, etc. + for (const arg of argv) { + if (arg.startsWith("-") && arg !== "-") { + for (const ch of arg.slice(1)) { + if (ch === "l") showLines = true; + else if (ch === "w") showWords = true; + else if (ch === "c") showBytes = true; + else { + // ignore unknown short options for this assignment + } + } + } else { + files.push(arg); + } + } + if (files.length === 0) { - console.error("Usage (this commit): node wc.js "); + console.error("Usage: node wc.js [-l|-w|-c] "); process.exit(1); } + // No flags → show all three like wc + const showAll = !showLines && !showWords && !showBytes; + let hadError = false; - let totalLines = 0, totalWords = 0, totalBytes = 0; + let total = { lines: 0, words: 0, bytes: 0 }; const results = []; for (const file of files) { @@ -25,9 +55,11 @@ async function main() { hadError = true; continue; } - const { lines, words, bytes } = await countFile(file); - results.push({ file, lines, words, bytes }); - totalLines += lines; totalWords += words; totalBytes += bytes; + const counts = await countFile(file); + results.push({ file, ...counts }); + total.lines += counts.lines; + total.words += counts.words; + total.bytes += counts.bytes; } catch (err) { if (err?.code === "ENOENT") { console.error(`wc: ${file}: No such file or directory`); @@ -41,10 +73,10 @@ async function main() { } for (const r of results) { - console.log(`${pad(r.lines)} ${pad(r.words)} ${pad(r.bytes)} ${r.file}`); + console.log(formatRow(r, { showAll, showLines, showWords, showBytes })); } if (results.length > 1) { - console.log(`${pad(totalLines)} ${pad(totalWords)} ${pad(totalBytes)} total`); + console.log(formatRow({ file: "total", ...total }, { showAll, showLines, showWords, showBytes })); } if (hadError) process.exitCode = 1; @@ -54,19 +86,27 @@ async function countFile(file) { const buf = await fs.promises.readFile(file); // Buffer const bytes = buf.length; - // lines: count '\n' bytes + // Lines: count '\n' bytes let lines = 0; for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) lines++; - // words: sequences of non-whitespace (on UTF-8 text) + // Words: sequences of non-whitespace const text = buf.toString("utf8"); const words = (text.match(/\S+/g) || []).length; return { lines, words, bytes }; } +function formatRow({ lines, words, bytes, file }, opts) { + const cols = []; + if (opts.showAll || opts.showLines) cols.push(pad(lines)); + if (opts.showAll || opts.showWords) cols.push(pad(words)); + if (opts.showAll || opts.showBytes) cols.push(pad(bytes)); + return `${cols.join(" ")} ${file}`; +} + function pad(n) { - // Right-align like `wc` (fixed width works well for visual parity) + // Right-align like `wc` return String(n).padStart(7, " "); }