diff --git a/implement-shell-tools/cat/README.md b/implement-shell-tools/cat/README.md index 7284a5e67..71a08ca9b 100644 --- a/implement-shell-tools/cat/README.md +++ b/implement-shell-tools/cat/README.md @@ -6,11 +6,11 @@ Your task is to implement your own version of `cat`. It must act the same as `cat` would, if run from the directory containing this README.md file, for the following command lines: -* `cat sample-files/1.txt` -* `cat -n sample-files/1.txt` -* `cat sample-files/*.txt` -* `cat -n sample-files/*.txt` -* `cat -b sample-files/3.txt` +- `cat sample-files/1.txt` +- `cat -n sample-files/1.txt` +- `cat sample-files/*.txt` +- `cat -n sample-files/*.txt` +- `cat -b sample-files/3.txt` Matching any additional behaviours or flags are optional stretch goals. diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..085ab3a6b --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,53 @@ +import process from "node:process"; +import { promises as fs } from "node:fs"; +import { program } from "commander"; + +program + .option("-n, --number", "number all output lines") + .option("-b, --number-nonblank", "number only non-empty lines") + .arguments("") + .parse(); + +const cliOptions = program.opts(); +const filePathsToRead = program.args; + +async function readAndOutputFiles() { + try { + const fileContents = await Promise.all( + filePathsToRead.map((filePath) => fs.readFile(filePath, "utf-8")), + ); + const concatenatedContent = fileContents.join(""); + + if (cliOptions.number) { + // apply -n logic: number all lines + const contentLines = concatenatedContent.split("\n"); + const numberedOutput = contentLines + .map((line, index) => { + return `${String(index + 1).padStart(6)} ${line}`; + }) + .join("\n"); + process.stdout.write(numberedOutput); + } else if (cliOptions.numberNonblank) { + // apply -b logic: number only non-empty lines + const contentLines = concatenatedContent.split("\n"); + let nonblankLineNumber = 0; + const numberedOutput = contentLines + .map((line) => { + if (line.trim() === "") { + return line; + } + nonblankLineNumber++; + return `${String(nonblankLineNumber).padStart(6)} ${line}`; + }) + .join("\n"); + process.stdout.write(numberedOutput); + } else { + process.stdout.write(concatenatedContent); + } + } catch (err) { + console.error("Error reading multiple files:", err); + process.exitCode = 1; + } +} + +readAndOutputFiles(); 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. diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..4af28962e --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,66 @@ +import process from "node:process"; +import { promises as fs } from "node:fs"; +import { program } from "commander"; + +program + .option("-1, --one-per-line", "list one file per line") + .option("-a, --all", "do not ignore entries starting with .") + .parse(); + +const cliOptions = program.opts(); +const cliArguments = program.args; + +async function runLsCommand() { + try { + // determine directory path (use current directory when none provided) + let directoryPath; + if (cliArguments.length === 0) { + directoryPath = "."; + } else { + directoryPath = cliArguments[0]; + } + + // read directory entries + const directoryEntries = await fs.readdir(directoryPath); + + // filter out dotfiles unless --all was provided + const visibleEntries = []; + if (cliOptions.all) { + for (const name of directoryEntries) { + visibleEntries.push(name); + } + } else { + for (const name of directoryEntries) { + if (!name.startsWith(".")) { + visibleEntries.push(name); + } + } + } + + // build output + let outputString = ""; + if (cliOptions.onePerLine) { + for (const name of visibleEntries) { + outputString += name + "\n"; + } + // if there are no entries, outputString stays empty + } else { + for (let i = 0; i < visibleEntries.length; i++) { + if (i > 0) { + outputString += " "; + } + outputString += visibleEntries[i]; + } + if (outputString !== "") { + outputString += "\n"; + } + } + + process.stdout.write(outputString); + } catch (err) { + console.error("Error reading directory:", err); + process.exitCode = 1; + } +} + +runLsCommand(); diff --git a/implement-shell-tools/package-lock.json b/implement-shell-tools/package-lock.json new file mode 100644 index 000000000..f0c81cdf1 --- /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..f8abbc98c --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.3" + } +} diff --git a/implement-shell-tools/wc/README.md b/implement-shell-tools/wc/README.md index bd76b655a..c4b76b643 100644 --- a/implement-shell-tools/wc/README.md +++ b/implement-shell-tools/wc/README.md @@ -6,11 +6,11 @@ Your task is to implement your own version of `wc`. It must act the same as `wc` would, if run from the directory containing this README.md file, for the following command lines: -* `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/*` +- `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/*` Matching any additional behaviours or flags are optional stretch goals. diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..6774e5f38 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,69 @@ +import process from "node:process"; +import { promises as fs } from "node:fs"; +import { Command } from "commander"; + +const program = new Command(); + +program + .name("wc") + .description("A simple node implementation of the word count utility") + .argument("[files...]", "Files to process") + .option("-l, --lines", "print the newline counts") + .option("-w, --words", "print the word counts") + .option("-c, --bytes", "print the byte counts") + .action(async (filePaths, options) => { + const noFlagsProvided = !options.lines && !options.words && !options.bytes; + const shouldShowAllStats = noFlagsProvided; + + const allFileStats = []; + + for (const filePath of filePaths) { + try { + const fileStats = await calculateFileStats(filePath); + allFileStats.push(fileStats); + printFormattedReport(fileStats, options, shouldShowAllStats); + } catch (error) { + console.error(`wc: ${filePath}: No such file or directory`); + process.exitCode = 1; + } + } + + if (allFileStats.length > 1) { + const grandTotals = { + lineCount: allFileStats.reduce((sum, stat) => sum + stat.lineCount, 0), + wordCount: allFileStats.reduce((sum, stat) => sum + stat.wordCount, 0), + byteCount: allFileStats.reduce((sum, stat) => sum + stat.byteCount, 0), + displayName: "total" + }; + printFormattedReport(grandTotals, options, shouldShowAllStats); + } + }); + +async function calculateFileStats(filePath) { + const fileBuffer = await fs.readFile(filePath); + const fileContent = fileBuffer.toString(); + + const lines = fileContent.split("\n").length - 1; + const words = fileContent.split(/\s+/).filter(word => word.length > 0).length; + const bytes = fileBuffer.length; + + return { + lineCount: lines, + wordCount: words, + byteCount: bytes, + displayName: filePath + }; +} + +function printFormattedReport(stats, options, shouldShowAllStats) { + const outputColumns = []; + const formatColumn = (count) => String(count).padStart(4); + + if (shouldShowAllStats || options.lines) outputColumns.push(formatColumn(stats.lineCount)); + if (shouldShowAllStats || options.words) outputColumns.push(formatColumn(stats.wordCount)); + if (shouldShowAllStats || options.bytes) outputColumns.push(formatColumn(stats.byteCount)); + + console.log(`${outputColumns.join("")} ${stats.displayName}`); +} + +program.parse(process.argv); \ No newline at end of file