From efeab1c12b2524e1e729d0e0f765915e87bbb461 Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Sun, 29 Mar 2026 16:47:03 +0100 Subject: [PATCH 1/7] cat exercise --- implement-shell-tools/cat/cat.js | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 implement-shell-tools/cat/cat.js diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..6ea795ad9 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,64 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program + .name("cat") + .description("concatenate and print files") + .option("-n, --number", "number all output lines") + .option("-b, --number-nonblank", "number nonempty output lines") + .argument("", "the file paths to process"); + +program.parse(); + +const options = program.opts(); +const paths = program.args; + +const numberAll = options.number; +const numberNonBlank = options.numberNonblank; + +const shouldNumberAll = numberNonBlank ? false : numberAll; + +let lineNumber = 1; +let nonBlankNumber = 1; + +for (const filePath of paths) { + try { + const content = await fs.readFile(filePath, "utf8"); + process.stdout.write(formatContent(content)); + } catch (error) { + process.exitCode = 1; + process.stderr.write(`cat: ${filePath}: ${error.message}\n`); + } +} + +function formatContent(text) { + const normalized = text.replace(/\r\n/g, "\n"); + const endsWithNewline = normalized.endsWith("\n"); + const lines = endsWithNewline + ? normalized.slice(0, -1).split("\n") + : normalized.split("\n"); + + const output = []; + + for (const line of lines) { + if (numberNonBlank) { + if (line === "") { + output.push(""); + } else { + output.push(`${String(nonBlankNumber++).padStart(6, " ")}\t${line}`); + } + } else if (shouldNumberAll) { + output.push(`${String(lineNumber++).padStart(6, " ")}\t${line}`); + } else { + output.push(line); + } + } + + let result = output.join("\n"); + + if (endsWithNewline) { + result += "\n"; + } + + return result; +} From 99454531f345b3b908662fb1948c0c12cdabd7f8 Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Mon, 30 Mar 2026 18:58:40 +0100 Subject: [PATCH 2/7] finished ls exercise --- implement-shell-tools/cat/package-lock.json | 6 +++ implement-shell-tools/cat/package.json | 1 + implement-shell-tools/ls/ls.js | 48 +++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 implement-shell-tools/cat/package-lock.json create mode 100644 implement-shell-tools/cat/package.json create mode 100644 implement-shell-tools/ls/ls.js diff --git a/implement-shell-tools/cat/package-lock.json b/implement-shell-tools/cat/package-lock.json new file mode 100644 index 000000000..1e1473cac --- /dev/null +++ b/implement-shell-tools/cat/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "cat", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/cat/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1 @@ +{} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..21ec869e3 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,48 @@ +import { promises as fs } from "node:fs"; +import { program } from "commander"; + +program + .name("list files and directories") + .option("-a", "show hidden files") + .option("-1", "force displaying each item in a new line") + .argument("", "path of directory"); + +program.parse(); + +const showHiddenFiles = program.opts()["a"]; +const showFilesInLines = program.opts()["1"]; +const fetchedDirectories = await fetchDirectoriesFunc(program.args); + +console.log(formatDisplay(fetchedDirectories)); + +function formatDisplay(fetchedDirectories) { + const controlDisplaying = fetchedDirectories.map((directoryFilesInArray) => { + const joiner = showFilesInLines ? `\n\r` : ` `; + const showFolderName = + fetchedDirectories.length > 1 + ? `${directoryFilesInArray.folderName}:\n\r` + : ""; + + return `${showFolderName}${directoryFilesInArray.files.join(joiner)}`; + }); + return controlDisplaying.join("\n\r\n\r"); +} + +// returns array of objects, like: [{folderName: [file1, file2]}] +async function fetchDirectoriesFunc(directories) { + const result = []; + + for (const folderName of directories) { + let files = await fs.readdir(folderName); + + if (showHiddenFiles) files.unshift(".", ".."); + + files.sort((a, b) => a.localeCompare(b)); + result.push({ + folderName, + files, + }); + } + + return result; +} From 94a9be835a7ba22181323545c8d93b8075422499 Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Mon, 30 Mar 2026 20:51:46 +0100 Subject: [PATCH 3/7] fixed ls --- implement-shell-tools/ls/ls.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index 21ec869e3..4490ebd0a 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -5,13 +5,15 @@ program .name("list files and directories") .option("-a", "show hidden files") .option("-1", "force displaying each item in a new line") - .argument("", "path of directory"); + .argument("[paths...]", "path of directory"); program.parse(); const showHiddenFiles = program.opts()["a"]; const showFilesInLines = program.opts()["1"]; -const fetchedDirectories = await fetchDirectoriesFunc(program.args); +const paths = program.args.length ? program.args : ["."]; + +const fetchedDirectories = await fetchDirectoriesFunc(paths); console.log(formatDisplay(fetchedDirectories)); @@ -35,9 +37,27 @@ async function fetchDirectoriesFunc(directories) { for (const folderName of directories) { let files = await fs.readdir(folderName); - if (showHiddenFiles) files.unshift(".", ".."); + // sort by name + but those starting with . at the end + files.sort((a, b) => { + const isHiddenA = a.startsWith("."); + const isHiddenB = b.startsWith("."); + + if (isHiddenA !== isHiddenB) { + return isHiddenA ? 1 : -1; + } + + const cleanA = a.replace(/^\.+/, ""); + const cleanB = b.replace(/^\.+/, ""); + + return cleanA.localeCompare(cleanB); + }); + + if (showHiddenFiles) { + files.unshift(".", ".."); + } else { + files = files.filter((fileName) => !fileName.startsWith(".")); + } - files.sort((a, b) => a.localeCompare(b)); result.push({ folderName, files, From cfd9881ef7962c3119faa573f82adaabf3dcf9ab Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Mon, 30 Mar 2026 20:57:25 +0100 Subject: [PATCH 4/7] unchanged readme.md --- implement-shell-tools/ls/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/implement-shell-tools/ls/README.md b/implement-shell-tools/ls/README.md index edbfb811a..d6355d0e5 100644 --- a/implement-shell-tools/ls/README.md +++ b/implement-shell-tools/ls/README.md @@ -6,9 +6,9 @@ Your task is to implement your own version of `ls`. It must act the same as `ls` would, if run from the directory containing this README.md file, for the following command lines: -* `ls -1` -* `ls -1 sample-files` -* `ls -1 -a sample-files` +- `ls -1` +- `ls -1 sample-files` +- `ls -1 -a sample-files` Matching any additional behaviours or flags are optional stretch goals. From c5b6059f9697e990627ba6a997d87cb65f543234 Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Wed, 1 Apr 2026 23:36:50 +0100 Subject: [PATCH 5/7] partially solved wc.js --- implement-shell-tools/package-lock.json | 21 ++++++++++++ implement-shell-tools/package.json | 6 ++++ implement-shell-tools/wc/wc.js | 43 +++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 implement-shell-tools/package-lock.json create mode 100644 implement-shell-tools/package.json create mode 100644 implement-shell-tools/wc/wc.js diff --git a/implement-shell-tools/package-lock.json b/implement-shell-tools/package-lock.json new file mode 100644 index 000000000..8cd4b3de8 --- /dev/null +++ b/implement-shell-tools/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "implement-shell-tools", + "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/package.json b/implement-shell-tools/package.json new file mode 100644 index 000000000..402b2a4e9 --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "commander": "^14.0.3" + }, + "type": "module" +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..bd5ff44f3 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,43 @@ +import { promises as fs } from "node:fs"; +import { program } from "commander"; +import { get } from "node:http"; + +program + .name("print newline, word, and byte counts for each file") + .option("-a", "to do something") + .argument("", "file name"); + +program.parse(); + +const paths = program.args; + +async function getNoFlagsOutput(paths) { + let lineCountTotal = 0; + let wordCountTotal = 0; + let fileSizeTotal = 0; + let output = []; + + for (const filename of paths) { + try { + const file = await fs.stat(filename); + + if (file.isFile()) { + const fileContent = await fs.readFile(filename, "utf-8"); + const lineCount = fileContent.match(/\n/g).length; + const wordCount = fileContent + .trim() + .split(/\s+/) + .filter(Boolean).length; + const fileSize = file.size; + + output.push([lineCount, wordCount, fileSize, filename]); + } + } catch (err) { + output.push([filename, `wc: ${filename} ${err.message}`]); + } + } + + return output; +} +//console.log(`${lineCountTotal} ${wordCountTotal} ${fileSizeTotal}Total`); +console.log(JSON.stringify(await getNoFlagsOutput(paths))); From 441ed0469f99688db3d53ba7b092558f4c79fa29 Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Sat, 4 Apr 2026 08:21:28 +0100 Subject: [PATCH 6/7] finished wc --- implement-shell-tools/wc/wc.js | 59 ++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index bd5ff44f3..d2b5086b2 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -1,20 +1,21 @@ import { promises as fs } from "node:fs"; import { program } from "commander"; -import { get } from "node:http"; program .name("print newline, word, and byte counts for each file") - .option("-a", "to do something") + .option("-l", "count lines") + .option("-w", "count words") + .option("-c", "count bytes") .argument("", "file name"); program.parse(); +const opts = program.opts(); +const flags = Object.keys(opts); +const columns = flags.length > 0 ? flags : ["l", "w", "c"]; const paths = program.args; -async function getNoFlagsOutput(paths) { - let lineCountTotal = 0; - let wordCountTotal = 0; - let fileSizeTotal = 0; +async function getFilesData(paths) { let output = []; for (const filename of paths) { @@ -23,14 +24,16 @@ async function getNoFlagsOutput(paths) { if (file.isFile()) { const fileContent = await fs.readFile(filename, "utf-8"); - const lineCount = fileContent.match(/\n/g).length; + const lineCount = (fileContent.match(/\n/g) || []).length; const wordCount = fileContent .trim() .split(/\s+/) .filter(Boolean).length; const fileSize = file.size; - output.push([lineCount, wordCount, fileSize, filename]); + output.push([filename, lineCount, wordCount, fileSize]); + } else { + output.push([filename, `wc: ${filename}: Is a directory`]); } } catch (err) { output.push([filename, `wc: ${filename} ${err.message}`]); @@ -39,5 +42,41 @@ async function getNoFlagsOutput(paths) { return output; } -//console.log(`${lineCountTotal} ${wordCountTotal} ${fileSizeTotal}Total`); -console.log(JSON.stringify(await getNoFlagsOutput(paths))); + +function displayWcOutput(output, columns) { + const results = output.filter((entry) => entry.length === 4); + + const totalLines = results.reduce((sum, e) => sum + e[1], 0); + const totalWords = results.reduce((sum, e) => sum + e[2], 0); + const totalBytes = results.reduce((sum, e) => sum + e[3], 0); + + const allRows = + results.length > 1 + ? [...results, ["total", totalLines, totalWords, totalBytes]] + : results; + + const colMap = { l: 1, w: 2, c: 3 }; + const w = columns.map((col) => + Math.max(...allRows.map((e) => String(e[colMap[col]]).length)), + ); + + const formatRow = (entry) => + columns + .map((col, i) => String(entry[colMap[col]]).padStart(w[i])) + .join(" "); + + for (const entry of output) { + if (entry.length === 2) { + console.log(entry[1]); + } else { + console.log(`${formatRow(entry)} ${entry[0]}`); + } + } + + if (results.length > 1) { + const totalsRow = ["total", totalLines, totalWords, totalBytes]; + console.log(`${formatRow(totalsRow)} total`); + } +} + +displayWcOutput(await getFilesData(paths), columns); From 3b2d05e517642b2e9eda2aa90e1bbb260159cd9c Mon Sep 17 00:00:00 2001 From: Mohammed Abdoon Date: Sat, 4 Apr 2026 09:00:53 +0100 Subject: [PATCH 7/7] rearranged files --- implement-shell-tools/cat/package-lock.json | 6 ------ implement-shell-tools/cat/package.json | 1 - 2 files changed, 7 deletions(-) delete mode 100644 implement-shell-tools/cat/package-lock.json delete mode 100644 implement-shell-tools/cat/package.json diff --git a/implement-shell-tools/cat/package-lock.json b/implement-shell-tools/cat/package-lock.json deleted file mode 100644 index 1e1473cac..000000000 --- a/implement-shell-tools/cat/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "cat", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/cat/package.json deleted file mode 100644 index 0967ef424..000000000 --- a/implement-shell-tools/cat/package.json +++ /dev/null @@ -1 +0,0 @@ -{}