diff --git a/.gitignore b/.gitignore index 3c3629e64..5da303acc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +**/.venv +**/requirements.txt diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..6011dca38 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,62 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program + .name("node-cat") + .description("A Node.js implementation of the Unix cat command") + .option("-n, --number", "Number all output lines") + .option( + "-b, --numberNonBlank", + "Numbers only non-empty lines. Overrides -n option" + ) + .argument("", "The file path to process"); + +program.parse(); + +const paths = program.args; +const { number, numberNonBlank } = program.opts(); + +// --- Read files --- +let content = ""; + +for (const path of paths) { + content += await fs.readFile(path, "utf-8"); +} + +// Remove the trailing newline +// I do realise that this is not exactly how cat works, but for the files that we have, we get a trailing new line and this makes the output look just as it would with the Unix cat command. +if (content.endsWith("\n")) { + content = content.slice(0, -1); +} + +const contentLines = content.split("\n"); + +// --- Numbering functions --- +function numberAll(lines) { + return lines.map( + (line, index) => `${String(index + 1).padStart(6, " ")} ${line}` + ); +} + +function numberNonEmpty(lines) { + let lineCounter = 1; + return lines.map((line) => + line.trim() === "" + ? line + : `${String(lineCounter++).padStart(6, " ")} ${line}` + ); +} + +// --- Output logic --- +let output; + +if (numberNonBlank) { + output = numberNonEmpty(contentLines); +} else if (number) { + output = numberAll(contentLines); +} else { + output = contentLines; +} + +// --- Print output --- +console.log(output.join("\n")); diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..c5cba5df2 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,32 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program + .name("node-ls") + .description("A Node.js implementation of the Unix ls command") + .option("-1", "list one file per line") + .option( + "-a, --all", + "include directory entries whose names begin with a dot (.)" + ) + .argument("[directory]", "The file path to process"); +program.parse(); + +const { 1: onePerLine, all } = program.opts(); +const directory = program.args[0] ? program.args[0] : "."; + +let entries = await fs.readdir(directory); + +// If -a is used, I've included "." and ".." to mimic what the Unix ls does +if (all) { + entries = [".", "..", ...entries]; +} else { + // hide dotfiles + entries = entries.filter((entry) => entry[0] !== "."); +} + +if (onePerLine) { + console.log(entries.join("\n")); +} else { + console.log(entries.join(" ")); +} diff --git a/implement-shell-tools/package-lock.json b/implement-shell-tools/package-lock.json new file mode 100644 index 000000000..09b3f387a --- /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.2" + } + }, + "node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "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..76dcd3f7a --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.2" + } +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..cec57ac95 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,100 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program + .name("node-wc") + .description("A Node.js implementation of the Unix wc command") + .option("-l, --lines", "Print the newline counts") + .option("-w, --words", "Print the word counts") + .option("-c, --bytes", "Print the byte counts") + .argument("", "The file path to process"); + +program.parse(); + +const filePaths = program.args; +const { lines, words, bytes } = program.opts(); + +// When no options are provided, show all counts +const showAll = !lines && !words && !bytes; + +// --- Read files and sizes --- +let fileContent = ""; +let outputData = []; + +let lineCountTotal = 0, + wordCountTotal = 0, + fileSizeTotal = 0; + +for (const path of filePaths) { + let fileStats; + let fileData = {}; + + // Count of flags and arguments provided -- basically state + fileData.countOfFlags = Object.values(program.opts()).filter(Boolean).length; + fileData.filePaths = filePaths.length; + + fileContent = await fs.readFile(path, "utf-8"); + + fileData.lineCount = getLineCount(fileContent); + lineCountTotal += fileData.lineCount; + + fileData.wordCount = getWordCount(fileContent); + wordCountTotal += fileData.wordCount; + + fileStats = await fs.stat(path); + fileData.fileSize = fileStats.size; + fileSizeTotal += fileData.fileSize; + + fileData.path = path; + outputData.push(fileData); +} + +console.log(outputData.map(formatOutput).join("\n")); + +if (filePaths.length > 1) { + console.log( + formatOutput({ + lineCount: lineCountTotal, + wordCount: wordCountTotal, + fileSize: fileSizeTotal, + path: "total", + }) + ); +} + +function formatOutput({ + lineCount, + wordCount, + fileSize, + path, + countOfFlags, + filePaths, +}) { + let output = []; + + // I've added this if statement as I found my node wc output looked misaligned compared to the Unix wc output when only one flag and one file were provided. + if (countOfFlags === 1 && filePaths <= 1) { + if (lines || showAll) output.push(String(lineCount)); + if (words || showAll) output.push(String(wordCount)); + if (bytes || showAll) output.push(String(fileSize)); + } else { + if (lines || showAll) output.push(String(lineCount).padStart(3)); + if (words || showAll) output.push(String(wordCount).padStart(4)); + if (bytes || showAll) output.push(String(fileSize).padStart(4)); + } + + return `${output.join("")} ${path}`; +} + +function getWordCount(text) { + let words, lines; + + lines = text.split("\n"); + words = lines.flatMap((line) => line.split(" ")); + + return words.filter((word) => word.length > 0).length; +} + +function getLineCount(text) { + return (text.match(/\n/g) || []).length; +}