From b40010c645867b8db6aff5d8f0cf96a0bfb9a194 Mon Sep 17 00:00:00 2001 From: Konvaly Date: Fri, 17 Apr 2026 01:13:34 +0100 Subject: [PATCH 1/5] implement cat with -n and -b flags --- implement-shell-tools/cat/cat.js | 54 ++++++++++++++++++++++++++ implement-shell-tools/cat/package.json | 3 ++ 2 files changed, 57 insertions(+) create mode 100644 implement-shell-tools/cat/cat.js create mode 100644 implement-shell-tools/cat/package.json diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..1b491c1a3 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,54 @@ +import { promises as fs } from "node:fs"; +import process from "node:process"; + +const args = process.argv.slice(2); + +if (args.length === 0) { + console.error("Usage: node cat.js [-n] [-b] "); + process.exit(1); +} +const showAllLineNumbers = args.includes("-n"); +const showNonBlankNumbers = args.includes("-b"); + +const filePaths = args.filter((arg) => !arg.startsWith("-")); + +let lineNumber = 1; + +for (const filePath of filePaths) { + const content = await fs.readFile(filePath, "utf-8"); + + if (!showAllLineNumbers && !showNonBlankNumbers) { + process.stdout.write(content); + } else { + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + let isLastLine; + if (i === lines.length - 1) { + isLastLine = true; + } else { + isLastLine = false; + } + + if (isLastLine && line === "") { + break; + } + + if (showAllLineNumbers) { + const paddedNumber = String(lineNumber).padStart(6, " "); + process.stdout.write(`${paddedNumber}\t${line}\n`); + lineNumber++; + } else if (showNonBlankNumbers) { + if (line.trim() === "") { + process.stdout.write("\n"); + } else { + const paddedNumber = String(lineNumber).padStart(6, " "); + process.stdout.write(`${paddedNumber}\t${line}\n`); + lineNumber++; + } + } + } + } +} diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/cat/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/implement-shell-tools/cat/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} From 19cae55bdd4fff5643ba0d95fe6947ec63a284d0 Mon Sep 17 00:00:00 2001 From: Konvaly Date: Fri, 17 Apr 2026 01:51:43 +0100 Subject: [PATCH 2/5] implement ls with -1 and -a flags --- implement-shell-tools/ls/ls.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 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..cbaef06e6 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,30 @@ +import { promises as fs } from "node:fs"; +import process from "node:process"; + +const args = process.argv.slice(2); + +const showHidden = args.includes("-a"); + +const paths = args.filter((arg) => !arg.startsWith("-")); + +const targetPath = paths.length > 0 ? paths[0] : process.cwd(); + +let entries = await fs.readdir(targetPath); + +if (!showHidden) { + entries = entries.filter((entry) => !entry.startsWith(".")); +} + +if (showHidden) { + entries = [".", "..", ...entries]; +} + +entries.sort((a, b) => { + const cleanA = a.replace(/^\.+/, ""); + const cleanB = b.replace(/^\.+/, ""); + return cleanA.localeCompare(cleanB); +}); + +for (const entry of entries) { + process.stdout.write(entry + "\n"); +} From ccf9aa69c70d943f4cea4793562c0df2d9fa8cd7 Mon Sep 17 00:00:00 2001 From: Konvaly Date: Fri, 17 Apr 2026 02:09:49 +0100 Subject: [PATCH 3/5] implement wc with -l -w -c flags --- implement-shell-tools/wc/wc.js | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 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..a0086ebdb --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,82 @@ +import { promises as fs } from "node:fs"; +import process from "node:process"; + +const args = process.argv.slice(2); + +const showLines = args.includes("-l"); +const showWords = args.includes("-w"); +const showBytes = args.includes("-c"); + +const noSpecificFlag = !showLines && !showWords && !showBytes; + +const filePaths = args.filter((arg) => !arg.startsWith("-")); + +if (filePaths.length === 0) { + console.error("Usage: node wc.js [-l] [-w] [-c] "); + process.exit(1); +} + +const results = []; + +for (const filePath of filePaths) { + const content = await fs.readFile(filePath, "utf-8"); + + const lines = content.endsWith("\n") + ? content.split("\n").length - 1 + : content.split("\n").length; + + const words = content.split(/\s+/).filter((w) => w.length > 0).length; + + const bytes = Buffer.byteLength(content, "utf-8"); + + results.push({ filePath, lines, words, bytes }); +} + +const totalLines = results.reduce((sum, r) => sum + r.lines, 0); +const totalWords = results.reduce((sum, r) => sum + r.words, 0); +const totalBytes = results.reduce((sum, r) => sum + r.bytes, 0); + +let maxNumber; + +if (noSpecificFlag) { + maxNumber = Math.max(totalLines, totalWords, totalBytes); +} else if (showLines) { + maxNumber = totalLines; +} else if (showWords) { + maxNumber = totalWords; +} else { + maxNumber = totalBytes; +} + +const width = String(maxNumber).length + 2; + +function formatLine(counts, label) { + const parts = counts.map((n) => String(n).padStart(width, " ")); + return parts.join("") + " " + label; +} + +for (const { filePath, lines, words, bytes } of results) { + if (noSpecificFlag) { + process.stdout.write(formatLine([lines, words, bytes], filePath) + "\n"); + } else if (showLines) { + process.stdout.write(formatLine([lines], filePath) + "\n"); + } else if (showWords) { + process.stdout.write(formatLine([words], filePath) + "\n"); + } else if (showBytes) { + process.stdout.write(formatLine([bytes], filePath) + "\n"); + } +} + +if (results.length > 1) { + if (noSpecificFlag) { + process.stdout.write( + formatLine([totalLines, totalWords, totalBytes], "total") + "\n", + ); + } else if (showLines) { + process.stdout.write(formatLine([totalLines], "total") + "\n"); + } else if (showWords) { + process.stdout.write(formatLine([totalWords], "total") + "\n"); + } else if (showBytes) { + process.stdout.write(formatLine([totalBytes], "total") + "\n"); + } +} From f3923c8e87d0fb01f32cf5efe94922dbc5a7f0a2 Mon Sep 17 00:00:00 2001 From: Konvaly Date: Mon, 27 Apr 2026 23:45:06 +0100 Subject: [PATCH 4/5] address PR feedback: unknown flags, combined flags, reduce repetition --- implement-shell-tools/wc/wc.js | 73 ++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js index a0086ebdb..a22c8c262 100644 --- a/implement-shell-tools/wc/wc.js +++ b/implement-shell-tools/wc/wc.js @@ -3,13 +3,34 @@ import process from "node:process"; const args = process.argv.slice(2); -const showLines = args.includes("-l"); -const showWords = args.includes("-w"); -const showBytes = args.includes("-c"); +const expandedArgs = []; +for (const arg of args) { + if (arg.startsWith("-") && arg.length > 2) { + for (const char of arg.slice(1)) { + expandedArgs.push(`-${char}`); + } + } else { + expandedArgs.push(arg); + } +} + +const showLines = expandedArgs.includes("-l"); +const showWords = expandedArgs.includes("-w"); +const showBytes = expandedArgs.includes("-c"); + +const supportedFlags = ["-l", "-w", "-c"]; +const unknownFlags = expandedArgs.filter( + (arg) => arg.startsWith("-") && !supportedFlags.includes(arg), +); + +if (unknownFlags.length > 0) { + console.error(`wc: invalid option -- '${unknownFlags[0].slice(1)}'`); + process.exit(1); +} const noSpecificFlag = !showLines && !showWords && !showBytes; -const filePaths = args.filter((arg) => !arg.startsWith("-")); +const filePaths = expandedArgs.filter((arg) => !arg.startsWith("-")); if (filePaths.length === 0) { console.error("Usage: node wc.js [-l] [-w] [-c] "); @@ -36,19 +57,17 @@ const totalLines = results.reduce((sum, r) => sum + r.lines, 0); const totalWords = results.reduce((sum, r) => sum + r.words, 0); const totalBytes = results.reduce((sum, r) => sum + r.bytes, 0); -let maxNumber; - -if (noSpecificFlag) { - maxNumber = Math.max(totalLines, totalWords, totalBytes); -} else if (showLines) { - maxNumber = totalLines; -} else if (showWords) { - maxNumber = totalWords; -} else { - maxNumber = totalBytes; +function getCounts(lines, words, bytes) { + const counts = []; + if (noSpecificFlag || showLines) counts.push(lines); + if (noSpecificFlag || showWords) counts.push(words); + if (noSpecificFlag || showBytes) counts.push(bytes); + return counts; } -const width = String(maxNumber).length + 2; +const maxNumber = Math.max(...getCounts(totalLines, totalWords, totalBytes)); + +const width = String(maxNumber).length + 1; function formatLine(counts, label) { const parts = counts.map((n) => String(n).padStart(width, " ")); @@ -56,27 +75,11 @@ function formatLine(counts, label) { } for (const { filePath, lines, words, bytes } of results) { - if (noSpecificFlag) { - process.stdout.write(formatLine([lines, words, bytes], filePath) + "\n"); - } else if (showLines) { - process.stdout.write(formatLine([lines], filePath) + "\n"); - } else if (showWords) { - process.stdout.write(formatLine([words], filePath) + "\n"); - } else if (showBytes) { - process.stdout.write(formatLine([bytes], filePath) + "\n"); - } + const counts = getCounts(lines, words, bytes); + process.stdout.write(formatLine(counts, filePath) + "\n"); } if (results.length > 1) { - if (noSpecificFlag) { - process.stdout.write( - formatLine([totalLines, totalWords, totalBytes], "total") + "\n", - ); - } else if (showLines) { - process.stdout.write(formatLine([totalLines], "total") + "\n"); - } else if (showWords) { - process.stdout.write(formatLine([totalWords], "total") + "\n"); - } else if (showBytes) { - process.stdout.write(formatLine([totalBytes], "total") + "\n"); - } + const totals = getCounts(totalLines, totalWords, totalBytes); + process.stdout.write(formatLine(totals, "total") + "\n"); } From 5c84e3170ff7fe3b4ac918feab475185deeede3a Mon Sep 17 00:00:00 2001 From: Konvaly Date: Tue, 28 Apr 2026 00:07:51 +0100 Subject: [PATCH 5/5] address PR feedback: unknown flags check, simplify cat logic, support multiple directories in ls --- implement-shell-tools/cat/cat.js | 32 +++++----- implement-shell-tools/ls/ls.js | 67 ++++++++++++++------ implement-shell-tools/{cat => }/package.json | 0 3 files changed, 65 insertions(+), 34 deletions(-) rename implement-shell-tools/{cat => }/package.json (100%) diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js index 1b491c1a3..aa3c74d84 100644 --- a/implement-shell-tools/cat/cat.js +++ b/implement-shell-tools/cat/cat.js @@ -9,6 +9,15 @@ if (args.length === 0) { } const showAllLineNumbers = args.includes("-n"); const showNonBlankNumbers = args.includes("-b"); +const supportedFlags = ["-n", "-b"]; +const unknownFlags = args.filter( + (arg) => arg.startsWith("-") && !supportedFlags.includes(arg), +); + +if (unknownFlags.length > 0) { + console.error(`cat: invalid option -- '${unknownFlags[0].slice(1)}'`); + process.exit(1); +} const filePaths = args.filter((arg) => !arg.startsWith("-")); @@ -25,29 +34,22 @@ for (const filePath of filePaths) { for (let i = 0; i < lines.length; i++) { const line = lines[i]; - let isLastLine; - if (i === lines.length - 1) { - isLastLine = true; - } else { - isLastLine = false; - } + const isLastLine = i === lines.length - 1; if (isLastLine && line === "") { break; } - if (showAllLineNumbers) { + const isBlankLine = line.trim() === ""; + const needsLineNumber = + showAllLineNumbers || (showNonBlankNumbers && !isBlankLine); + + if (needsLineNumber) { const paddedNumber = String(lineNumber).padStart(6, " "); process.stdout.write(`${paddedNumber}\t${line}\n`); lineNumber++; - } else if (showNonBlankNumbers) { - if (line.trim() === "") { - process.stdout.write("\n"); - } else { - const paddedNumber = String(lineNumber).padStart(6, " "); - process.stdout.write(`${paddedNumber}\t${line}\n`); - lineNumber++; - } + } else if (showNonBlankNumbers && isBlankLine) { + process.stdout.write("\n"); } } } diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js index cbaef06e6..bced7ea05 100644 --- a/implement-shell-tools/ls/ls.js +++ b/implement-shell-tools/ls/ls.js @@ -5,26 +5,55 @@ const args = process.argv.slice(2); const showHidden = args.includes("-a"); -const paths = args.filter((arg) => !arg.startsWith("-")); - -const targetPath = paths.length > 0 ? paths[0] : process.cwd(); - -let entries = await fs.readdir(targetPath); - -if (!showHidden) { - entries = entries.filter((entry) => !entry.startsWith(".")); +const supportedFlags = ["-1", "-a"]; +const unknownFlags = args.filter( + (arg) => arg.startsWith("-") && !supportedFlags.includes(arg), +); + +if (unknownFlags.length > 0) { + console.error(`ls: invalid option -- '${unknownFlags[0].slice(1)}'`); + process.exit(1); } -if (showHidden) { - entries = [".", "..", ...entries]; -} - -entries.sort((a, b) => { - const cleanA = a.replace(/^\.+/, ""); - const cleanB = b.replace(/^\.+/, ""); - return cleanA.localeCompare(cleanB); -}); +const paths = args.filter((arg) => !arg.startsWith("-")); -for (const entry of entries) { - process.stdout.write(entry + "\n"); +const targetPaths = paths.length > 0 ? paths : [process.cwd()]; + +for (const targetPath of targetPaths) { + if (targetPaths.length > 1) { + process.stdout.write(`${targetPath}:\n`); + } + + let entries; + + try { + entries = await fs.readdir(targetPath); + } catch (error) { + console.error( + `ls: cannot access '${targetPath}': No such file or directory`, + ); + continue; + } + + if (!showHidden) { + entries = entries.filter((entry) => !entry.startsWith(".")); + } + + if (showHidden) { + entries = [".", "..", ...entries]; + } + + entries.sort((a, b) => { + const cleanA = a.replace(/^\.+/, ""); + const cleanB = b.replace(/^\.+/, ""); + return cleanA.localeCompare(cleanB); + }); + + for (const entry of entries) { + process.stdout.write(entry + "\n"); + } + + if (targetPaths.length > 1) { + process.stdout.write("\n"); + } } diff --git a/implement-shell-tools/cat/package.json b/implement-shell-tools/package.json similarity index 100% rename from implement-shell-tools/cat/package.json rename to implement-shell-tools/package.json