From 3de450eac5fcd907f86ecbf3ce852324c265f6b7 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Tue, 2 Dec 2025 22:07:23 +0000 Subject: [PATCH 1/7] Intalled commader library; Create cat.js file --- implement-shell-tools/cat/.gitignore | 2 ++ implement-shell-tools/cat/cat.js | 27 +++++++++++++++++++ implement-shell-tools/cat/ls/README.md | 15 +++++++++++ .../cat/ls/sample-files/.hidden.txt | 0 .../cat/ls/sample-files/1.txt | 1 + .../cat/ls/sample-files/2.txt | 1 + .../cat/ls/sample-files/3.txt | 5 ++++ .../cat/ls/sample-files/dir/a.txt | 0 .../cat/ls/sample-files/dir/b.txt | 0 .../cat/ls/sample-files/dir/subdir/x.txt | 0 .../cat/ls/sample-files/dir/subdir/y.txt | 0 .../cat/ls/sample-files/dir/subdir/z.txt | 0 implement-shell-tools/cat/package-lock.json | 21 +++++++++++++++ implement-shell-tools/cat/package.json | 6 +++++ 14 files changed, 78 insertions(+) create mode 100644 implement-shell-tools/cat/.gitignore create mode 100644 implement-shell-tools/cat/cat.js create mode 100644 implement-shell-tools/cat/ls/README.md create mode 100644 implement-shell-tools/cat/ls/sample-files/.hidden.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/1.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/2.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/3.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/dir/a.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/dir/b.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/dir/subdir/x.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/dir/subdir/y.txt create mode 100644 implement-shell-tools/cat/ls/sample-files/dir/subdir/z.txt create mode 100644 implement-shell-tools/cat/package-lock.json create mode 100644 implement-shell-tools/cat/package.json diff --git a/implement-shell-tools/cat/.gitignore b/implement-shell-tools/cat/.gitignore new file mode 100644 index 000000000..f31e3d79c --- /dev/null +++ b/implement-shell-tools/cat/.gitignore @@ -0,0 +1,2 @@ +# Dependency directory +/node_modules diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..62d8593f2 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,27 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import process from "node:process"; + +program + .name("count-containing-words") + .description("Counts words in a file that contain a particular character") + .option("-c, --char ", "The character to search for", "e") + .argument("", "The file path to process"); + +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 path = argv[0]; +const char = program.opts().char; + +const content = await fs.readFile(path, "utf-8"); +const countOfWordsContainingChar = content + .split(" ") + .filter((word) => word.includes(char)).length; +console.log(countOfWordsContainingChar); diff --git a/implement-shell-tools/cat/ls/README.md b/implement-shell-tools/cat/ls/README.md new file mode 100644 index 000000000..edbfb811a --- /dev/null +++ b/implement-shell-tools/cat/ls/README.md @@ -0,0 +1,15 @@ +# Implement `ls` + +You should already be familiar with the `ls` command line tool. + +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` + +Matching any additional behaviours or flags are optional stretch goals. + +We recommend you start off supporting just `-1`, then adding support for `-a`. diff --git a/implement-shell-tools/cat/ls/sample-files/.hidden.txt b/implement-shell-tools/cat/ls/sample-files/.hidden.txt new file mode 100644 index 000000000..e69de29bb diff --git a/implement-shell-tools/cat/ls/sample-files/1.txt b/implement-shell-tools/cat/ls/sample-files/1.txt new file mode 100644 index 000000000..ed7bf7d08 --- /dev/null +++ b/implement-shell-tools/cat/ls/sample-files/1.txt @@ -0,0 +1 @@ +Once upon a time... diff --git a/implement-shell-tools/cat/ls/sample-files/2.txt b/implement-shell-tools/cat/ls/sample-files/2.txt new file mode 100644 index 000000000..c2b8f0d39 --- /dev/null +++ b/implement-shell-tools/cat/ls/sample-files/2.txt @@ -0,0 +1 @@ +There was a house made of gingerbread. diff --git a/implement-shell-tools/cat/ls/sample-files/3.txt b/implement-shell-tools/cat/ls/sample-files/3.txt new file mode 100644 index 000000000..cdba09987 --- /dev/null +++ b/implement-shell-tools/cat/ls/sample-files/3.txt @@ -0,0 +1,5 @@ +It looked delicious. +I was tempted to take a bite of it. +But this seemed like a bad idea... + +There's more to come, though... diff --git a/implement-shell-tools/cat/ls/sample-files/dir/a.txt b/implement-shell-tools/cat/ls/sample-files/dir/a.txt new file mode 100644 index 000000000..e69de29bb diff --git a/implement-shell-tools/cat/ls/sample-files/dir/b.txt b/implement-shell-tools/cat/ls/sample-files/dir/b.txt new file mode 100644 index 000000000..e69de29bb diff --git a/implement-shell-tools/cat/ls/sample-files/dir/subdir/x.txt b/implement-shell-tools/cat/ls/sample-files/dir/subdir/x.txt new file mode 100644 index 000000000..e69de29bb diff --git a/implement-shell-tools/cat/ls/sample-files/dir/subdir/y.txt b/implement-shell-tools/cat/ls/sample-files/dir/subdir/y.txt new file mode 100644 index 000000000..e69de29bb diff --git a/implement-shell-tools/cat/ls/sample-files/dir/subdir/z.txt b/implement-shell-tools/cat/ls/sample-files/dir/subdir/z.txt new file mode 100644 index 000000000..e69de29bb diff --git a/implement-shell-tools/cat/package-lock.json b/implement-shell-tools/cat/package-lock.json new file mode 100644 index 000000000..11da220f0 --- /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.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/cat/package.json b/implement-shell-tools/cat/package.json new file mode 100644 index 000000000..76dcd3f7a --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.2" + } +} From 6dd0b3a7f8a484d939363bc3420ba98ab4c429d5 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Wed, 3 Dec 2025 00:08:11 +0000 Subject: [PATCH 2/7] implement cat command with -n and -b flag support --- implement-shell-tools/cat/cat.js | 55 +++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 62d8593f2..31ecce055 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -3,25 +3,42 @@ import { promises as fs } from "node:fs"; import process from "node:process"; program - .name("count-containing-words") - .description("Counts words in a file that contain a particular character") - .option("-c, --char ", "The character to search for", "e") - .argument("", "The file path to process"); + .name("display-file-content") + .description("Implement cat command with -n and -b flag support") + .option("-n, --number-all-lines", "Number every line in the file") + .option("-b, --number-non-empty-lines", "Number non empty lines in the file") + .argument("", "File paths to process"); -program.parse(); +program.parse(process.argv); -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 path = argv[0]; -const char = program.opts().char; +const args = program.args; //Array all file paths -const content = await fs.readFile(path, "utf-8"); -const countOfWordsContainingChar = content - .split(" ") - .filter((word) => word.includes(char)).length; -console.log(countOfWordsContainingChar); +//read flags user typed and return them as object. +const opts = program.opts(); + +let lineNumber = 1; + +// Loop over every filepath in args +args.forEach(async (filepath) => { + const content = await fs.readFile(filepath, "utf8"); + const lines = content.split("\n"); + + lines.forEach((line) => { + if (opts.numberAllLines) { + // -n: number every line + console.log(`${lineNumber} ${line}`); + lineNumber++; + } else if (opts.numberNonEmptyLines) { + // -b: number non-empty lines only + if (line.trim() === "") { + console.log(line); + } else { + console.log(`${lineNumber} ${line}`); + lineNumber++; + } + } else { + // No flag: just print + console.log(line); + } + }); +}); From 5684c3700dc79a8ed88261cc66edeb1d5181e0d0 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Thu, 4 Dec 2025 00:26:25 +0000 Subject: [PATCH 3/7] Implement ls command to list files in directory with -1 flag and -a flag --- implement-shell-tools/ls/ls.js | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 implement-shell-tools/ls/ls.js diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..9919dee20 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,42 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program + .name("list-files-in-directory") + .description("Implement ls command to list files in directory") + .option("-1, --one-per-line", "list files one per line") + .option("-a, --allFiles", "list all files including hidden ones") + .argument("[paths...]", "File paths to process"); + +program.parse(process.argv); + +// options and path from commander. +const opts = program.opts(); + +let paths = program.args; // array of paths user typed +if (paths.length === 0) { + paths = ["."]; +} + +for (const directoryPath of paths) { + let entries; + try { + entries = await fs.readdir(directoryPath); + } catch (err) { + console.error(`ls: cannot access '${directoryPath}': ${err.message}`); + continue; // move to next path if this one fails + } + + if (!opts.allFiles) { + entries = entries.filter((name) => !name.startsWith(".")); + } + + if (opts.onePerLine) { + entries.forEach((name) => { + console.log(name); + }); + } else { + //print files in one line if -1 is not used + console.log(entries.join(" ")); + } +} From 7a43ee808647387f8f8c9797f344233323cae67e Mon Sep 17 00:00:00 2001 From: ike-agu Date: Thu, 4 Dec 2025 18:21:07 +0000 Subject: [PATCH 4/7] Implement wc command to count lines, words and characters --- implement-shell-tools/wc/wc.js | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 implement-shell-tools/wc/wc.js diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..14895f605 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,80 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import process from "node:process"; + +program + .name("wc command") + .description("Implement wc command to count words and lines") + .option("-l,", "show number of lines only") + .option("-w,", "show number of words only") + .option("-c,", "show number of characters only") + .argument("[path...]", "The file path to process"); + +program.parse(process.argv); + +const opts = program.opts(); +let paths = program.args; + +if (paths.length === 0) { + console.error("wc: no file specified"); + process.exit(1); +} + +let totals = { lines: 0, words: 0, chars: 0 }; + +for (const filePath of paths) { + let content; + try { + content = await fs.readFile(filePath, "utf8"); + } catch (err) { + console.error(`wc: cannot read file "${filePath}": ${err.message}`); + continue; // move on to next path + } + + const lineCount = content.split("n").length; + + const wordCount = content.split(/\s+/).filter(Boolean).length; + + const charCount = content.length; + + totals.lines += lineCount; + totals.words += wordCount; + totals.chars += charCount; + + // Decide what to print depending on flags + if (!opts.l && !opts.w && !opts.c) { + console.log(`${lineCount} ${wordCount} ${charCount} ${filePath}`); + continue; + } + + if (opts.l) { + console.log(`${lineCount} ${filePath}`); + } + + if (opts.w) { + console.log(`${wordCount} ${filePath}`); + } + + if (opts.c) { + console.log(`${charCount} ${filePath}`); + } +} + +//Print totals if there are multiple files +if (paths.length > 1) { + if (!opts.l && !opts.w && !opts.c) { + console.log(`${totals.lines} ${totals.words} ${totals.chars} total`); + } + + if (opts.l) { + console.log(`${totals.lines} total`); + } + + if (opts.w) { + console.log(`${totals.words} total`); + } + + if (opts.c) { + console.log(`${totals.chars} total`); + } +} From 0acf2ca008ed21e1dd271ddb00cb28cb6f63592c Mon Sep 17 00:00:00 2001 From: ike-agu Date: Tue, 10 Feb 2026 15:02:03 +0000 Subject: [PATCH 5/7] refactor: improve readability by Deleting comments and replacing forEach with for...of loops --- implement-shell-tools/cat/cat.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 31ecce055..e761d144e 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -11,34 +11,32 @@ program program.parse(process.argv); -const args = program.args; //Array all file paths +const filepaths = program.args; -//read flags user typed and return them as object. -const opts = program.opts(); +const options = program.opts(); let lineNumber = 1; -// Loop over every filepath in args -args.forEach(async (filepath) => { - const content = await fs.readFile(filepath, "utf8"); - const lines = content.split("\n"); +for (const filepath of filepaths) { + const fileContent = await fs.readFile(filepath, "utf8"); + const lines = fileContent.split("\n"); - lines.forEach((line) => { - if (opts.numberAllLines) { - // -n: number every line + for (const line of lines) { + if (options.numberAllLines) { console.log(`${lineNumber} ${line}`); lineNumber++; - } else if (opts.numberNonEmptyLines) { - // -b: number non-empty lines only + continue; + } + + if (options.numberNonEmptyLines) { if (line.trim() === "") { console.log(line); } else { console.log(`${lineNumber} ${line}`); lineNumber++; } - } else { - // No flag: just print - console.log(line); + continue; } - }); -}); + console.log(line); + } +} From 3c7c4f7ba7ac6ecb489d6b352f78aab46a421b33 Mon Sep 17 00:00:00 2001 From: ike-agu Date: Tue, 10 Feb 2026 15:30:46 +0000 Subject: [PATCH 6/7] refactor: improve naming and simplify ls filtering logic --- implement-shell-tools/ls/ls.js | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index 9919dee20..e62ce5606 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -10,33 +10,30 @@ program program.parse(process.argv); -// options and path from commander. -const opts = program.opts(); +const options = program.opts(); -let paths = program.args; // array of paths user typed -if (paths.length === 0) { - paths = ["."]; -} +let paths = program.args; +if (paths.length === 0) paths = ["."]; for (const directoryPath of paths) { - let entries; + let fileNames; + try { - entries = await fs.readdir(directoryPath); + fileNames = await fs.readdir(directoryPath); } catch (err) { console.error(`ls: cannot access '${directoryPath}': ${err.message}`); - continue; // move to next path if this one fails + continue; } - if (!opts.allFiles) { - entries = entries.filter((name) => !name.startsWith(".")); - } + fileNames = fileNames.filter( + (file) => options.allFiles || !file.startsWith("."), + ); - if (opts.onePerLine) { - entries.forEach((name) => { - console.log(name); - }); + if (options.onePerLine) { + for (const file of fileNames) { + console.log(file); + } } else { - //print files in one line if -1 is not used - console.log(entries.join(" ")); + console.log(fileNames.join(" ")); } } From c192601b14665b84c897cd7e5680144ff2f1a69b Mon Sep 17 00:00:00 2001 From: ike-agu Date: Tue, 10 Feb 2026 17:01:03 +0000 Subject: [PATCH 7/7] fix wc flags and align behavior with Unix wc --- implement-shell-tools/wc/wc.js | 63 +++++++++++++--------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index 14895f605..66cd286d4 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -1,14 +1,13 @@ import { program } from "commander"; import { promises as fs } from "node:fs"; -import process from "node:process"; program .name("wc command") .description("Implement wc command to count words and lines") - .option("-l,", "show number of lines only") - .option("-w,", "show number of words only") - .option("-c,", "show number of characters only") - .argument("[path...]", "The file path to process"); + .option("-l, --lines", "show number of lines only") + .option("-w, --words", "show number of words only") + .option("-c, --chars", "show number of characters only") + .argument("[paths...]", "The file path to process"); program.parse(process.argv); @@ -16,8 +15,7 @@ const opts = program.opts(); let paths = program.args; if (paths.length === 0) { - console.error("wc: no file specified"); - process.exit(1); + paths = ["."]; } let totals = { lines: 0, words: 0, chars: 0 }; @@ -28,53 +26,38 @@ for (const filePath of paths) { content = await fs.readFile(filePath, "utf8"); } catch (err) { console.error(`wc: cannot read file "${filePath}": ${err.message}`); - continue; // move on to next path + continue; } - const lineCount = content.split("n").length; - + const lineCount = content.split("\n").length; const wordCount = content.split(/\s+/).filter(Boolean).length; - const charCount = content.length; totals.lines += lineCount; totals.words += wordCount; totals.chars += charCount; - // Decide what to print depending on flags - if (!opts.l && !opts.w && !opts.c) { - console.log(`${lineCount} ${wordCount} ${charCount} ${filePath}`); - continue; - } - - if (opts.l) { - console.log(`${lineCount} ${filePath}`); - } - - if (opts.w) { - console.log(`${wordCount} ${filePath}`); - } + const output = []; - if (opts.c) { - console.log(`${charCount} ${filePath}`); + if (!opts.lines && !opts.words && !opts.chars) { + output.push(lineCount, wordCount, charCount); + } else { + if (opts.lines) output.push(lineCount); + if (opts.words) output.push(wordCount); + if (opts.chars) output.push(charCount); } + console.log(`${output.join(" ")} ${filePath}`); } -//Print totals if there are multiple files if (paths.length > 1) { - if (!opts.l && !opts.w && !opts.c) { - console.log(`${totals.lines} ${totals.words} ${totals.chars} total`); - } - - if (opts.l) { - console.log(`${totals.lines} total`); - } - - if (opts.w) { - console.log(`${totals.words} total`); - } + const totalOutput = []; - if (opts.c) { - console.log(`${totals.chars} total`); + if (!opts.lines && !opts.words && !opts.chars) { + totalOutput.push(totals.lines, totals.words, totals.chars); + } else { + if (opts.lines) totalOutput.push(totals.lines); + if (opts.words) totalOutput.push(totals.words); + if (opts.chars) totalOutput.push(totals.chars); } + console.log(`${totalOutput.join(" ")} total`); }