diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..04ab1dcc9 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,38 @@ +import { program } from "commander"; +import * as fs from "node:fs/promises"; + +program + .name("cat") + .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 filePaths = program.args; + + const options = program.opts(); + + for (const filePath of filePaths) { + const file = await fs.open(filePath); + + let lineNum = 1; + + 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); + + if (shouldNumber) lineNum++; + } + } finally { + await file.close(); + } + } +} 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" + } +} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..3034da06d --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,67 @@ +import { program } from "commander"; +import * as fs from "node:fs/promises"; + +program + .name("ls") + .description("List directory contents") + .argument( + "[paths...]", + "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."); + +program.parse(); + +try { + let filePaths = program.args; + if (!filePaths || filePaths.length === 0) { + filePaths = ["."]; + } + + const options = program.opts(); + 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 filterHidden = (files) => files.filter((file) => !file.startsWith(".")); + + const getVisibleEntries = (files) => + includeHidden ? files : filterHidden(files); + + 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 { + 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); +} 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" + } +} 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..a10798e32 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,65 @@ +import { program } from "commander"; +import * as fs from "node:fs/promises"; + +program + .name("wc") + .description("word, line and byte count") + .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(); + +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++; + const trimmed = line.trim(); + if (trimmed.length > 0) { + count.words += trimmed.split(/\s+/).length; + } + } + } finally { + await file.close(); + } + results[filePath] = count; + } + + 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; + } + + 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); +}