From 7045902ca90cfd1128f44f7ccb6cef927c787b4d Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:57:54 +0000 Subject: [PATCH 01/11] feat(cat): add initial implementation of cat command to read and output file contents --- implement-shell-tools/cat/cat.js | 17 +++++++++++++++++ implement-shell-tools/cat/package-lock.json | 21 +++++++++++++++++++++ implement-shell-tools/cat/package.json | 6 ++++++ 3 files changed, 44 insertions(+) create mode 100644 implement-shell-tools/cat/cat.js create mode 100644 implement-shell-tools/cat/package-lock.json create mode 100644 implement-shell-tools/cat/package.json diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..5c67a908e --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,17 @@ +import { program } from "commander"; +import * as fs from "node:fs/promises"; + +program + .name("cat") + .description("Reads file and writes it to the standard output") + .argument("", "The file path to process"); + +program.parse(); + +try { + const [filePath] = program.args; + const contents = await fs.readFile(filePath, { encoding: "utf8" }); + console.log(contents); +} catch (err) { + console.error(err.message); +} diff --git a/implement-shell-tools/cat/package-lock.json b/implement-shell-tools/cat/package-lock.json new file mode 100644 index 000000000..6179cab28 --- /dev/null +++ b/implement-shell-tools/cat/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "cat", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "commander": "^14.0.3" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + } + } +} diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/cat/package.json new file mode 100644 index 000000000..043047a15 --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.3" + } +} From d373c0a57f2fb5d30c8cf50dab368d0b7c8e683b Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:28:09 +0000 Subject: [PATCH 02/11] feat(cat): enhance cat command with optional flags --- implement-shell-tools/cat/cat.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 5c67a908e..6ecc56b34 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -4,14 +4,27 @@ import * as fs from "node:fs/promises"; program .name("cat") .description("Reads file and writes it to the standard output") - .argument("", "The file path to process"); + .argument("", "The file path to process") + .option("-n", "Number the output lines, starting at 1.") + .option("-b", "Number only non-blank output lines, starting at 1."); program.parse(); try { const [filePath] = program.args; - const contents = await fs.readFile(filePath, { encoding: "utf8" }); - console.log(contents); + const options = program.opts(); + + const file = await fs.open(filePath); + + let lineNum = 1; + for await (const line of file.readLines()) { + const isBlank = line.trim() === ""; + const shouldNumber = options.n || (options.b && !isBlank); + + console.log(shouldNumber ? `${lineNum} ${line}` : line); + + if (shouldNumber) lineNum++; + } } catch (err) { console.error(err.message); } From 975035e4eb9a0b9f7a81330aaabffa148a8be8a5 Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:02:07 +0000 Subject: [PATCH 03/11] feat(cat): update cat command to handle multiple file paths --- implement-shell-tools/cat/cat.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 6ecc56b34..04ab1dcc9 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -3,27 +3,35 @@ import * as fs from "node:fs/promises"; program .name("cat") - .description("Reads file and writes it to the standard output") - .argument("", "The file path to process") + .description("Reads file(s) and writes them to the standard output") + .argument("", "The file path(s) to process") .option("-n", "Number the output lines, starting at 1.") .option("-b", "Number only non-blank output lines, starting at 1."); program.parse(); try { - const [filePath] = program.args; + const filePaths = program.args; + const options = program.opts(); - const file = await fs.open(filePath); + for (const filePath of filePaths) { + const file = await fs.open(filePath); + + let lineNum = 1; - let lineNum = 1; - for await (const line of file.readLines()) { - const isBlank = line.trim() === ""; - const shouldNumber = options.n || (options.b && !isBlank); + try { + for await (const line of file.readLines()) { + const isBlank = line.trim() === ""; + const shouldNumber = options.n || (options.b && !isBlank); - console.log(shouldNumber ? `${lineNum} ${line}` : line); + console.log(shouldNumber ? `${lineNum} ${line}` : line); - if (shouldNumber) lineNum++; + if (shouldNumber) lineNum++; + } + } finally { + await file.close(); + } } } catch (err) { console.error(err.message); From d4a4f43923616f80d4a369ebe48c9dd2a134d3fb Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 05:52:35 +0000 Subject: [PATCH 04/11] feat(ls): add initial ls command to list current working directory contents --- implement-shell-tools/ls/ls.js | 27 ++++++++++++++++++++++ implement-shell-tools/ls/package-lock.json | 21 +++++++++++++++++ implement-shell-tools/ls/package.json | 6 +++++ 3 files changed, 54 insertions(+) create mode 100644 implement-shell-tools/ls/ls.js create mode 100644 implement-shell-tools/ls/package-lock.json create mode 100644 implement-shell-tools/ls/package.json diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..7c1be50c5 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,27 @@ +import { program } from "commander"; +import * as fs from "node:fs/promises"; + +program + .name("ls") + .description("List directory contents") + .option("-a", "Include directory entries whose names begin with a dot ('.').") + .option("-1", "Force output to be one entry per line."); + +program.parse(); + +try { + const options = program.opts(); + + const currentDir = process.cwd(); + const files = await fs.readdir(currentDir); + + if (options["1"]) { + for (const file of files) { + console.log(file); + } + } else { + console.log(...files); + } +} catch (err) { + console.error(err.message); +} diff --git a/implement-shell-tools/ls/package-lock.json b/implement-shell-tools/ls/package-lock.json new file mode 100644 index 000000000..082762764 --- /dev/null +++ b/implement-shell-tools/ls/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "ls", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "commander": "^14.0.3" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + } + } +} diff --git a/implement-shell-tools/ls/package.json b/implement-shell-tools/ls/package.json new file mode 100644 index 000000000..043047a15 --- /dev/null +++ b/implement-shell-tools/ls/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.3" + } +} From 9f1261f26dbf23b97753849c2290e6339f633fa6 Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:16:36 +0000 Subject: [PATCH 05/11] feat(ls): add argument support for specifying directory path --- implement-shell-tools/ls/ls.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index 7c1be50c5..847a74c14 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -4,6 +4,10 @@ import * as fs from "node:fs/promises"; program .name("ls") .description("List directory contents") + .argument( + "[path]", + "The file path to process (defaults to current directory)", + ) .option("-a", "Include directory entries whose names begin with a dot ('.').") .option("-1", "Force output to be one entry per line."); @@ -11,6 +15,10 @@ program.parse(); try { const options = program.opts(); + const [filePathArg] = program.args; + + const filePath = filePathArg || process.cwd(); + let files = await fs.readdir(filePath); const currentDir = process.cwd(); const files = await fs.readdir(currentDir); From 4abcb92186d0f1eb2f9760d571043bbb389c7549 Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:17:38 +0000 Subject: [PATCH 06/11] feat(ls): add logic to filter hidden files --- implement-shell-tools/ls/ls.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index 847a74c14..3a45b8c1c 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -20,8 +20,7 @@ try { const filePath = filePathArg || process.cwd(); let files = await fs.readdir(filePath); - const currentDir = process.cwd(); - const files = await fs.readdir(currentDir); + if (!options.a) files = files.filter((file) => !file.startsWith(".")); if (options["1"]) { for (const file of files) { From c6a7649af312483ba110fd8fa5a4a2e4049e0e3d Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:11:53 +0000 Subject: [PATCH 07/11] feat(wc): implement wc command for word, line, and byte counting --- implement-shell-tools/wc/package-lock.json | 21 ++++++++++++ implement-shell-tools/wc/package.json | 6 ++++ implement-shell-tools/wc/wc.js | 39 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 implement-shell-tools/wc/package-lock.json create mode 100644 implement-shell-tools/wc/package.json create mode 100644 implement-shell-tools/wc/wc.js diff --git a/implement-shell-tools/wc/package-lock.json b/implement-shell-tools/wc/package-lock.json new file mode 100644 index 000000000..c598ee9ca --- /dev/null +++ b/implement-shell-tools/wc/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "wc", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "commander": "^14.0.3" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + } + } +} diff --git a/implement-shell-tools/wc/package.json b/implement-shell-tools/wc/package.json new file mode 100644 index 000000000..043047a15 --- /dev/null +++ b/implement-shell-tools/wc/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.3" + } +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..30dd2607e --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,39 @@ +import { program } from "commander"; +import * as fs from "node:fs/promises"; + +//*** TODO *** +// * Format results +// * Add optional flags + +program + .name("wc") + .description("word, line and byte count") + .argument("", "The file path(s) to process"); + +program.parse(); + +try { + const filePaths = program.args; + const results = {}; + + for (const filePath of filePaths) { + const file = await fs.open(filePath); + const stats = await fs.stat(filePath); + + const count = { lines: 0, words: 0, bytes: stats.size }; + + try { + for await (const line of file.readLines()) { + count.lines++; + count.words += line.trim().split(/\s+/).length; + } + } finally { + await file.close(); + } + + results[filePath] = count; + } + console.log(results); +} catch (err) { + console.error(err.message); +} From fafe5fd78bdf69cfdf32c1ebf2e2a1d34985d2e1 Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:35:59 +0000 Subject: [PATCH 08/11] feat(wc): format results into table with total counts for multiple files --- implement-shell-tools/wc/wc.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index 30dd2607e..9c822618d 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -33,7 +33,18 @@ try { results[filePath] = count; } - console.log(results); + + if (filePaths.length > 1) { + const total = { lines: 0, words: 0, bytes: 0 }; + for (const file of Object.values(results)) { + total.lines += file.lines; + total.words += file.words; + total.bytes += file.bytes; + } + results["total"] = total; + } + + console.table(results); } catch (err) { console.error(err.message); } From 0071dc4100cef86012954c1ae2a70f100d0386ff Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:56:06 +0000 Subject: [PATCH 09/11] feat(wc): add optional flags for line, word, and byte counting --- implement-shell-tools/wc/wc.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index 9c822618d..428e3337c 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -1,14 +1,22 @@ import { program } from "commander"; import * as fs from "node:fs/promises"; -//*** TODO *** -// * Format results -// * Add optional flags - program .name("wc") .description("word, line and byte count") - .argument("", "The file path(s) to process"); + .argument("", "The file path(s) to process.") + .option( + "-l, --lines", + "The number of lines in each input file is written to the standard output.", + ) + .option( + "-w, --words", + "The number of words in each input file is written to the standard output.", + ) + .option( + "-c --bytes", + "The number of bytes in each input file is written to the standard output.", + ); program.parse(); @@ -19,7 +27,6 @@ try { for (const filePath of filePaths) { const file = await fs.open(filePath); const stats = await fs.stat(filePath); - const count = { lines: 0, words: 0, bytes: stats.size }; try { @@ -30,7 +37,6 @@ try { } finally { await file.close(); } - results[filePath] = count; } @@ -44,7 +50,13 @@ try { results["total"] = total; } - console.table(results); + const options = program.opts(); + const noOptionsProvided = !Object.keys(options).length; + const selectedOptionKeys = [...Object.keys(options)]; + + noOptionsProvided + ? console.table(results) + : console.table(results, selectedOptionKeys); } catch (err) { console.error(err.message); } From b9817fc3b9d93292df5f7e7f3e8307a1e4ae1fad Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:56:45 +0000 Subject: [PATCH 10/11] fix(wc): prevent counting empty lines as words --- implement-shell-tools/wc/wc.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index 428e3337c..a10798e32 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -32,7 +32,10 @@ try { try { for await (const line of file.readLines()) { count.lines++; - count.words += line.trim().split(/\s+/).length; + const trimmed = line.trim(); + if (trimmed.length > 0) { + count.words += trimmed.split(/\s+/).length; + } } } finally { await file.close(); From 3f81d49dcc86757edc4cef828b07f18f03848e53 Mon Sep 17 00:00:00 2001 From: Jak R-S <176810031+jakr-s@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:42:03 +0000 Subject: [PATCH 11/11] feat(ls): support multiple file paths and improve visibility filtering --- implement-shell-tools/ls/ls.js | 51 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index 3a45b8c1c..3034da06d 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -5,7 +5,7 @@ program .name("ls") .description("List directory contents") .argument( - "[path]", + "[paths...]", "The file path to process (defaults to current directory)", ) .option("-a", "Include directory entries whose names begin with a dot ('.').") @@ -14,20 +14,53 @@ program program.parse(); try { + let filePaths = program.args; + if (!filePaths || filePaths.length === 0) { + filePaths = ["."]; + } + const options = program.opts(); - const [filePathArg] = program.args; + const includeHidden = Boolean(options.a); + const onePerLine = Boolean(options["1"]); + + const result = { files: [], dirs: {} }; + + for (const filePath of filePaths) { + const stats = await fs.stat(filePath); + if (stats.isFile()) result.files.push(filePath); + if (stats.isDirectory()) { + result.dirs[filePath] = await fs.readdir(filePath); + } + } - const filePath = filePathArg || process.cwd(); - let files = await fs.readdir(filePath); + const filterHidden = (files) => files.filter((file) => !file.startsWith(".")); - if (!options.a) files = files.filter((file) => !file.startsWith(".")); + const getVisibleEntries = (files) => + includeHidden ? files : filterHidden(files); - if (options["1"]) { - for (const file of files) { - console.log(file); + const formatEntries = (files) => { + if (files.length === 0) return; + console.log(files.join(onePerLine ? "\n" : "\t")); + }; + + result.files = getVisibleEntries(result.files); + + if (filePaths.length === 1) { + let entries = [...result.files]; + + for (const [dir, contents] of Object.entries(result.dirs)) { + const filtered = getVisibleEntries(contents); + entries = entries.concat(filtered); } + formatEntries(entries); } else { - console.log(...files); + formatEntries(result.files); + + for (const [dir, contents] of Object.entries(result.dirs)) { + console.log("\n" + dir + ":"); + const filtered = getVisibleEntries(contents); + formatEntries(filtered); + } } } catch (err) { console.error(err.message);