diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs new file mode 100644 index 000000000..0b7e01a14 --- /dev/null +++ b/implement-shell-tools/cat/cat.mjs @@ -0,0 +1,62 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program.name("cat"); +program.description("simple cat clone"); +program.option("-n, --number", "number all output lines"); +program.option("-b, --number-nonblank", "number non-empty output lines"); +program.argument("[paths...]", "file(s) to process"); +program.parse(); + +const { number, numberNonblank } = program.opts(); +const paths = program.args; + +async function processFile(path) { + try { + const stat = await fs.stat(path); + + if (stat.isDirectory()) { + console.error(`${path}: Is a directory`); + return; + } + + const content = await fs.readFile(path, { encoding: "utf-8" }); + const lines = content.split("\n"); + const hasTrailingNewLine = content.endsWith("\n"); + const effectiveLines = hasTrailingNewLine ? lines.slice(0, -1) : lines; + let lineNumber = 1; + + const processed = effectiveLines.map((line) => { + if (numberNonblank) { + if (line.trim() !== "") { + const result = `${lineNumber}\t${line}`; + lineNumber += 1; + return result; + } + + return line; + } + + if (number) { + const result = `${lineNumber}\t${line}`; + lineNumber += 1; + return result; + } + + return line; + }); + + const output = processed.join("\n"); + console.log(output); + } catch (error) { + console.error(`${path}: ${error.message}`); + } +} + +try { + for (const path of paths) { + await processFile(path); + } +} catch (error) { + console.error(error.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..737542646 --- /dev/null +++ b/implement-shell-tools/cat/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "cat", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cat", + "version": "1.0.0", + "license": "ISC", + "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..f4e5c2a03 --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1,16 @@ +{ + "name": "cat", + "version": "1.0.0", + "type": "module", + "description": "You should already be familiar with the `cat` command line tool.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "commander": "^14.0.3" + } +} diff --git a/implement-shell-tools/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs new file mode 100644 index 000000000..4507ca4ba --- /dev/null +++ b/implement-shell-tools/ls/ls.mjs @@ -0,0 +1,45 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import process from "node:process"; + +program.name("ls"); +program.description("simple ls clone"); +program.option("-1", "list one file per line"); +program.option("-a", "show hidden files"); +program.argument(""); +program.parse(); + +const argv = program.args; + +if (argv.length != 1) { + console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}`); + process.exit(1); +} + +const options = program.opts(); +const path = argv[0]; + +try { + const stat = await fs.stat(path); + + if (stat.isDirectory()) { + let files = await fs.readdir(path); + + if (!options.a) { + files = files.filter((file) => !file.startsWith(".")); + } + + if (options["1"]) { + for (const file of files) { + console.log(file); + } + } else { + const output = files.join("\t"); + console.log(output); + } + } else { + console.log(path); + } +} catch (err) { + console.log(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..a7d9a0d28 --- /dev/null +++ b/implement-shell-tools/ls/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "commander", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "commander", + "version": "1.0.0", + "license": "ISC", + "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..428694b60 --- /dev/null +++ b/implement-shell-tools/ls/package.json @@ -0,0 +1,15 @@ +{ + "name": "commander", + "version": "1.0.0", + "type": "module", + "description": "You should already be familiar with the `ls` command line tool.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "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..8aa122ddd --- /dev/null +++ b/implement-shell-tools/wc/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "wc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wc", + "version": "1.0.0", + "license": "ISC", + "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..791a3741a --- /dev/null +++ b/implement-shell-tools/wc/package.json @@ -0,0 +1,16 @@ +{ + "name": "wc", + "version": "1.0.0", + "type": "module", + "description": "You should already be familiar with the `wc` command line tool.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "commander": "^14.0.3" + } +} diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs new file mode 100644 index 000000000..d656bf4dc --- /dev/null +++ b/implement-shell-tools/wc/wc.mjs @@ -0,0 +1,75 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program.name("wc"); +program.description("simple wc clone"); +program.option("-w, --words", "print word count"); +program.option("-l, --lines", "print line count"); +program.option("-c, --bytes", "print byte count"); +program.argument("[paths...]", "file(s) to process"); +program.parse(); + +const options = program.opts(); +const paths = program.args; + +const noFlags = !options.words && !options.lines && !options.bytes; +const showWords = options.words || noFlags; +const showLines = options.lines || noFlags; +const showBytes = options.bytes || noFlags; + +function count(content) { + const lines = content.split("\n").length - 1; + const words = content.trim().split(/\s+/).filter(Boolean).length; + const bytes = Buffer.byteLength(content); + + return { lines, words, bytes }; +} + +function formatCounts({ lines, words, bytes }, label = "") { + const output = []; + + if (showLines) { + output.push(lines.toString().padStart(8)); + } + + if (showWords) { + output.push(words.toString().padStart(8)); + } + + if (showBytes) { + output.push(bytes.toString().padStart(8)); + } + + if (label) { + output.push(label); + } + + return output.join(" "); +} + +async function processFile(path) { + try { + const stat = await fs.stat(path); + + if (stat.isDirectory()) { + console.error(`${path}: Is a directory`); + return; + } + + const content = await fs.readFile(path, { encoding: "utf-8" }); + const counts = count(content); + const output = formatCounts(counts, path); + + console.log(output); + } catch (err) { + console.error(`${path}: ${err.message}`); + } +} + +try { + for (const path of paths) { + await processFile(path); + } +} catch (err) { + console.error(err.message); +}