diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..ff592b5f0 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,65 @@ +import { readFileSync } from "node:fs"; +import process from "node:process"; + +// Note: takes only one flag, -n or -b, and accepts whatever the last flag is +// can parse flag from any position in args + +// capturing the user args +const args = process.argv.slice(2); + +let flag; +const paths = []; + +// the * (glob expansion is done automatically by zsh, bash etc. on linux) +for (const arg of args) { + if (arg === "-n" || arg === "-b") { + flag = arg; + } else { + paths.push(arg); + } +} + +// if no file is supplied exit with error +if (paths.length === 0) { + console.error("usage: cat [-n] "); + process.exit(1); +} + +// starting file number, if lines need to be prepended + +for (const path of paths) { + let lineNum = 1; + let file; + try { + // using sync as it's a simple short program + file = readFileSync(path, "utf-8"); + } catch (err) { + console.error(`cat: ${path}: ${err.message}`); + continue; // Real cat continues to next file if current file not found + } + + const lines = file.split("\n"); + + // remove trailing empty line as this how real cat works + if (lines[lines.length - 1] === "") lines.pop(); + + if (flag === "-n") { + for (const line of lines) { + console.log(`${lineNum} ${line}`); + lineNum++; + } + } else if (flag === "-b") { + for (const line of lines) { + if (line === "") { + console.log(line); + } else { + console.log(`${lineNum} ${line}`); + lineNum++; + } + } + } else { + for (const line of lines) { + console.log(line); + } + } +} diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/cat/package.json new file mode 100644 index 000000000..c927a8982 --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1,13 @@ +{ + "name": "cat", + "version": "1.0.0", + "description": "You should already be familiar with the `cat` command line tool.", + "main": "cat.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module" +} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..ddbc14e13 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,69 @@ +import fs from "node:fs"; +import process from "node:process"; + +const args = process.argv.slice(2); + +const flags = new Set(); +let paths = []; + +let isFlag = true; +for (const arg of args) { + if (isFlag && arg === "--") { + isFlag = false; + } else if (isFlag && arg.startsWith("-") && arg !== "-") { + // capture the flags without the - + // supports combined flags like -1a + for (const ch of arg.slice(1)) { + flags.add(ch); + } + } else { + paths.push(arg); + } +} + +if (paths.length === 0) { + paths.push("."); +} + +// returns all entries for a given path +// if -a flag, then include dotfiles, else exclude dotfiles +function getPathEntries(path, aFlag = flags.has("a")) { + let entries = fs.readdirSync(path); + + if (!aFlag) { + entries = entries.filter((e) => !e.startsWith(".")); + } + return entries; +} + +// formatter: if -1 flag, print entry per line +// else all in one line with +function printEntries(entries, onePerLineFlag = flags.has("1")) { + if (onePerLineFlag) { + entries.forEach((e) => console.log(e)); + } else { + // if join on empty entries arr, add extra blank line + if (entries.length !== 0) { + console.log(entries.join("\t")); + } + } +} + +// this is needed to group files at the top and folers at the bottom when giving mutiple path arguements +// to argv e.g. node ls.js sample-files/* +const fileArgs = paths.filter((p) => !fs.statSync(p).isDirectory()); +const dirArgs = paths.filter((p) => fs.statSync(p).isDirectory()); + +// First print all plain file arguments together, as one group +if (fileArgs.length > 0) { + printEntries(fileArgs); +} + +// Then print each directory's listing, with headers if needed +dirArgs.forEach((path, index) => { + if (paths.length > 1) { + if (index > 0 || fileArgs.length > 0) console.log(""); + console.log(`${path}:`); + } + printEntries(getPathEntries(path)); +}); diff --git a/implement-shell-tools/wc/package-lock.json b/implement-shell-tools/wc/package-lock.json new file mode 100644 index 000000000..11fb6ab6f --- /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": "^15.0.0" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + } + } +} diff --git a/implement-shell-tools/wc/package.json b/implement-shell-tools/wc/package.json new file mode 100644 index 000000000..489705b8d --- /dev/null +++ b/implement-shell-tools/wc/package.json @@ -0,0 +1,16 @@ +{ + "name": "wc", + "version": "1.0.0", + "description": "You should already be familiar with the `wc` command line tool.", + "main": "wc.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "commander": "^15.0.0" + } +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..3cb4c8cbc --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,75 @@ +import { program } from "commander"; +import fs, { chownSync } from "node:fs"; +import process from "node:process"; + +program + .name("wc") + .description("Mimics the wc command line tool") + .option("-w") + .option("-c") + .option("-l") + .argument("", "files to process"); + +program.parse(); + +const options = program.opts(); +const paths = program.args; + +// if no -lwc flags are supplied, wc prints +// lines, words, bytes of each file +// whereas if any flags are supplied only those +// values are printed +if (Object.keys(options).length === 0) { + options.l = options.w = options.c = true; +} + +// keeps track of total values for each of the data +// incrementally updated in the loop below +const totals = { l: 0, w: 0, c: 0 }; + +let fileCount = 0; +for (const path of paths) { + if (fs.statSync(path).isDirectory()) { + console.log(`wc: ${path}: read: Is a directory`); + } else { + fileCount++; + let outputStr = ""; + const file = fs.readFileSync(path, "utf-8"); + if (options.l) { + const lines = file.split("\n"); + // exclude trailing empty line from count + if (lines.at(-1) === "") { + lines.pop(); + } + const lineCount = lines.length; + totals.l += lineCount; + outputStr += `\t${lineCount}`; + } + + if (options.w) { + // real wc splits not just on " ", but on white spaces more generally + const words = file.split(/\s+/).filter(Boolean); + const wordCount = words.length; + totals.w += wordCount; + outputStr += `\t${wordCount}`; + } + + if (options.c) { + file.size; + const byteCount = fs.statSync(path).size; + totals.c += byteCount; + outputStr += `\t${byteCount}`; + } + + outputStr += ` ${path}`; + console.log(outputStr); + } +} + +// if there's more than one file, print out a total +// if a flag is not selected, then the value for that flag is 0 +// filter out anything with a total of 0 +if (fileCount > 1) { + const totalsArr = Object.values(totals).filter((elem) => elem !== 0); + console.log(`\t${totalsArr.join("\t")} total`); +}