From 45b8f91ce21bd60e55b7f3ddfa9dc19c5ea4f417 Mon Sep 17 00:00:00 2001 From: XiaoQuark Date: Sat, 25 Jul 2026 08:53:45 +0100 Subject: [PATCH 1/6] temp: commit for computer change --- implement-shell-tools/cat/cat-commander.mjs | 66 +++++++++++++++++++++ implement-shell-tools/cat/cat.mjs | 39 ++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 implement-shell-tools/cat/cat-commander.mjs create mode 100644 implement-shell-tools/cat/cat.mjs diff --git a/implement-shell-tools/cat/cat-commander.mjs b/implement-shell-tools/cat/cat-commander.mjs new file mode 100644 index 000000000..6f01b3d22 --- /dev/null +++ b/implement-shell-tools/cat/cat-commander.mjs @@ -0,0 +1,66 @@ +import process from "node:process"; +import { promises as fs } from "node:fs"; +import { Command } from "commander"; + +const program = new Command(); +program + .name("cat") + .description("Concatenate files and print them to stdout") + .option("-n, --number", "number all output lines") + .option("-b, --number-nonblank", "number non-blank lines") + .option( + "-r, --replace-empty-lines", + "Replaces multiple consecutive empty lines with one empty line", + ) + .argument("", "Paths to process"); + +program.parse(); + +const options = program.opts(); +const paths = program.args; + +console.log(options); + +for (const path of paths) { + let contents = await fs.readFile(path, "utf8"); + const lines = contents.split("\n"); + + if (options.replaceEmptyLines) { + contents = replaceEmptyLines(lines); + } + + if (options.numberNonblank) { + contents = numberLines(lines, false); + } else if (options.number) { + contents = numberLines(lines, true); + } + + process.stdout.write(contents); +} + +function numberLines(lines, includeBlankLines) { + let lineNumber = 1; + for (let i = 0; i < lines.length; i++) { + if (includeBlankLines || lines[i].length > 0) { + lines[i] = `${lineNumber} ${lines[i]}`; + lineNumber++; + } + } + + return lines.join("\n"); +} + +function replaceEmptyLines(lines) { + const result = []; + let previousLineWasBlank = false; + + for (let i; i < lines.length; i++) { + const currentLineIsBlank = lines[i].length === 0; + if (!currentLineIsBlank || !previousLineWasBlank) { + result.push(lines[i]); + } + previousLineWasBlank = currentLineIsBlank; + } + + return result.join("\n"); +} diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs new file mode 100644 index 000000000..940846d02 --- /dev/null +++ b/implement-shell-tools/cat/cat.mjs @@ -0,0 +1,39 @@ +import process from "node:process"; +import { promises as fs } from "node:fs"; + +const optionDefinitions = [ + { + short: "-n", + description: "Numbers all output lines", + }, + { + short: "-b", + description: "Numbers non-blank output lines", + }, +]; + +const argv = process.argv.slice(2); +// if (argv.length != 1) { +// console.error( +// `Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`, +// ); +// process.exit(1); +// } + +console.log(argv); + +const matchedOptions = optionDefinitions.find((option) => arg === option.short); +const paths = []; +for (const arg of argv) { + if (arg.match(flagRegex)) { + flags.push(arg); + } else { + paths.push(arg); + } +} +console.log(flags, paths); + +for (const path of paths) { + const contents = await fs.readFile(path, "utf8"); + process.stdout.write(contents); +} From a51051be837925f75ff53377f5124e00e81b45ab Mon Sep 17 00:00:00 2001 From: XiaoQuark Date: Mon, 27 Jul 2026 16:02:49 +0100 Subject: [PATCH 2/6] task: complete cat javascript implementation --- implement-shell-tools/cat/cat-commander.mjs | 66 ----------------- implement-shell-tools/cat/cat.mjs | 78 ++++++++++++--------- implement-shell-tools/package-lock.json | 20 ++++++ implement-shell-tools/package.json | 5 ++ 4 files changed, 71 insertions(+), 98 deletions(-) delete mode 100644 implement-shell-tools/cat/cat-commander.mjs create mode 100644 implement-shell-tools/package-lock.json create mode 100644 implement-shell-tools/package.json diff --git a/implement-shell-tools/cat/cat-commander.mjs b/implement-shell-tools/cat/cat-commander.mjs deleted file mode 100644 index 6f01b3d22..000000000 --- a/implement-shell-tools/cat/cat-commander.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import process from "node:process"; -import { promises as fs } from "node:fs"; -import { Command } from "commander"; - -const program = new Command(); -program - .name("cat") - .description("Concatenate files and print them to stdout") - .option("-n, --number", "number all output lines") - .option("-b, --number-nonblank", "number non-blank lines") - .option( - "-r, --replace-empty-lines", - "Replaces multiple consecutive empty lines with one empty line", - ) - .argument("", "Paths to process"); - -program.parse(); - -const options = program.opts(); -const paths = program.args; - -console.log(options); - -for (const path of paths) { - let contents = await fs.readFile(path, "utf8"); - const lines = contents.split("\n"); - - if (options.replaceEmptyLines) { - contents = replaceEmptyLines(lines); - } - - if (options.numberNonblank) { - contents = numberLines(lines, false); - } else if (options.number) { - contents = numberLines(lines, true); - } - - process.stdout.write(contents); -} - -function numberLines(lines, includeBlankLines) { - let lineNumber = 1; - for (let i = 0; i < lines.length; i++) { - if (includeBlankLines || lines[i].length > 0) { - lines[i] = `${lineNumber} ${lines[i]}`; - lineNumber++; - } - } - - return lines.join("\n"); -} - -function replaceEmptyLines(lines) { - const result = []; - let previousLineWasBlank = false; - - for (let i; i < lines.length; i++) { - const currentLineIsBlank = lines[i].length === 0; - if (!currentLineIsBlank || !previousLineWasBlank) { - result.push(lines[i]); - } - previousLineWasBlank = currentLineIsBlank; - } - - return result.join("\n"); -} diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs index 940846d02..431dfc1b0 100644 --- a/implement-shell-tools/cat/cat.mjs +++ b/implement-shell-tools/cat/cat.mjs @@ -1,39 +1,53 @@ import process from "node:process"; import { promises as fs } from "node:fs"; +import { Command } from "commander"; -const optionDefinitions = [ - { - short: "-n", - description: "Numbers all output lines", - }, - { - short: "-b", - description: "Numbers non-blank output lines", - }, -]; - -const argv = process.argv.slice(2); -// if (argv.length != 1) { -// console.error( -// `Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`, -// ); -// process.exit(1); -// } - -console.log(argv); - -const matchedOptions = optionDefinitions.find((option) => arg === option.short); -const paths = []; -for (const arg of argv) { - if (arg.match(flagRegex)) { - flags.push(arg); - } else { - paths.push(arg); - } -} -console.log(flags, paths); +const program = new Command(); +program + .name("cat") + .description("Concatenate files and print them to stdout") + .option("-n, --number", "number all output lines") + .option("-b, --number-nonblank", "number non-blank lines") + .argument("", "Paths to process"); + +program.parse(); + +const options = program.opts(); +const paths = program.args; +let lineNumber = 1; for (const path of paths) { const contents = await fs.readFile(path, "utf8"); - process.stdout.write(contents); + const endsWithNewline = contents.endsWith("\n"); + + let lines = contents.split("\n"); + + if (endsWithNewline) { + lines.pop(); + } + + if (options.numberNonblank) { + lines = numberLines(lines, false); + } else if (options.number) { + lines = numberLines(lines, true); + } + + let output = lines.join("\n"); + + if (endsWithNewline) { + output += "\n"; + } + + process.stdout.write(output); +} + +function numberLines(lines, includeBlankLines) { + for (let i = 0; i < lines.length; i++) { + if (includeBlankLines || lines[i].length > 0) { + lines[i] = `${String(lineNumber).padStart(6, " ")}\t${lines[i]}`; + lineNumber++; + } + } + + return lines; } diff --git a/implement-shell-tools/package-lock.json b/implement-shell-tools/package-lock.json new file mode 100644 index 000000000..928714cdf --- /dev/null +++ b/implement-shell-tools/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "implement-shell-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "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==", + "engines": { + "node": ">=22.12.0" + } + } + } +} diff --git a/implement-shell-tools/package.json b/implement-shell-tools/package.json new file mode 100644 index 000000000..2dd8f09a8 --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "commander": "^15.0.0" + } +} From 8ab9ff39be76470e6c15d3226022a1fd61ed13bb Mon Sep 17 00:00:00 2001 From: XiaoQuark Date: Mon, 27 Jul 2026 18:44:16 +0100 Subject: [PATCH 3/6] task: complete implementation of ls functionality in javascript --- implement-shell-tools/ls/ls.mjs | 110 ++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 implement-shell-tools/ls/ls.mjs diff --git a/implement-shell-tools/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs new file mode 100644 index 000000000..98fc45b3f --- /dev/null +++ b/implement-shell-tools/ls/ls.mjs @@ -0,0 +1,110 @@ +import process from "node:process"; +import fs, { truncate } from "node:fs"; + +const argv = process.argv.slice(2); + +const options = { + onePerLine: false, + all: false, + help: false, +}; + +const validFlags = { + onePerLine: { + short: "-1", + long: "--one-per-line", + description: "Lists one file per line", + }, + all: { + short: "-a", + long: "--all", + description: "Include entries whose names begin with a dot", + }, + help: { + short: "-h", + long: "--help", + description: "Display help information", + }, +}; + +const paths = []; +let parsingFlags = true; + +for (const arg of argv) { + if (arg === "--") { + parsingFlags = false; + } else if (parsingFlags && arg.startsWith("--")) { + const matchedFlag = Object.entries(validFlags).find( + ([, definition]) => arg === definition.long, + ); + + if (matchedFlag) { + const [optionName] = matchedFlag; + options[optionName] = true; + } else { + process.stderr.write(`Invalid option -- '${arg}'\n`); + process.exit(1); + } + } else if (parsingFlags && arg.startsWith("-")) { + const shortFlags = arg.slice(1); + + for (const shortFlag of shortFlags) { + const matchedFlag = Object.entries(validFlags).find( + ([, definition]) => definition.short === `-${shortFlag}`, + ); + + if (matchedFlag) { + const [optionName] = matchedFlag; + options[optionName] = true; + } else { + process.stderr.write(`Invalid option -- '${arg}\n'`); + process.exit(1); + } + } + } else { + paths.push(arg); + } +} + +if (paths.length === 0) { + paths.push("."); +} + +const multiplePaths = paths.length > 1; +const filePaths = []; +const directoryPaths = []; + +for (const path of paths) { + const stats = fs.statSync(path); + + if (stats.isDirectory()) { + directoryPaths.push(path); + } else { + filePaths.push(path); + } +} + +if (filePaths.length > 0) { + const separator = options.onePerLine ? "\n" : " "; + process.stdout.write(`${filePaths.join(separator)}\n`); +} + +for (const path of directoryPaths) { + if (multiplePaths) { + process.stdout.write(`\n${path}: \n`); + } + let content = fs.readdirSync(path); + + if (options.all) { + content.unshift(".", ".."); + } else { + content = content.filter((entry) => !entry.startsWith(".")); + } + + content.sort(); + + const separator = options.onePerLine ? "\n" : " "; + const output = content.join(separator); + + process.stdout.write(`${output}\n`); +} From fc1680b358f0056148c55a28d9276654fe9fa620 Mon Sep 17 00:00:00 2001 From: XiaoQuark Date: Mon, 27 Jul 2026 20:58:11 +0100 Subject: [PATCH 4/6] task: complete implementation of wc in javascript --- implement-shell-tools/wc/wc.mjs | 142 ++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 implement-shell-tools/wc/wc.mjs diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs new file mode 100644 index 000000000..81befa607 --- /dev/null +++ b/implement-shell-tools/wc/wc.mjs @@ -0,0 +1,142 @@ +import process from "node:process"; +import fs from "node:fs"; +import { parseArgs } from "node:util"; + +const options = { + lines: { + type: "boolean", + short: "l", + }, + words: { + type: "boolean", + short: "w", + }, + bytes: { + type: "boolean", + short: "c", + }, +}; + +const { values, positionals } = parseArgs({ + options, + allowPositionals: true, +}); + +if (positionals.length === 0) { + process.stderr.write("Please provide a file.\n"); + process.exit(1); +} + +const results = []; + +// read file and calculate counts +for (const path of positionals) { + const content = fs.readFileSync(path, "utf-8"); + + const lineCount = [...content].filter((char) => char === "\n").length; + + const trimmedContent = content.trim(); + + const wordCount = + trimmedContent === "" ? 0 : trimmedContent.split(/\s+/).length; + + const byteCount = Buffer.byteLength(content); + + results.push({ + path, + lineCount, + wordCount, + byteCount, + }); +} + +let totalLines = 0; +let totalWords = 0; +let totalBytes = 0; + +// calculate total counts +for (const result of results) { + totalLines += result.lineCount; + totalWords += result.wordCount; + totalBytes += result.byteCount; +} + +const allCounts = []; + +for (const result of results) { + const counts = getSelectedCounts( + result.lineCount, + result.wordCount, + result.byteCount, + ); + + allCounts.push(...counts); +} + +if (results.length > 1) { + const totalCounts = getSelectedCounts(totalLines, totalWords, totalBytes); + + allCounts.push(...totalCounts); +} + +// flags handling function +function getSelectedCounts(lineCount, wordCount, byteCount) { + const counts = []; + + if (values.lines) { + counts.push(lineCount); + } + + if (values.words) { + counts.push(wordCount); + } + + if (values.bytes) { + counts.push(byteCount); + } + + if (counts.length === 0) { + counts.push(lineCount, wordCount, byteCount); + } + + return counts; +} + +// output formatting +let largestByteCount = 0; + +for (const result of results) { + if (result.byteCount > largestByteCount) { + largestByteCount = result.byteCount; + } +} + +if (results.length > 1 && totalBytes > largestByteCount) { + largestByteCount = totalBytes; +} + +const width = String(largestByteCount).length; + +for (const result of results) { + const counts = getSelectedCounts( + result.lineCount, + result.wordCount, + result.byteCount, + ); + + const formattedCounts = counts + .map((count) => String(count).padStart(width)) + .join(" "); + + process.stdout.write(`${formattedCounts} ${result.path}\n`); +} + +if (results.length > 1) { + const totalCounts = getSelectedCounts(totalLines, totalWords, totalBytes); + + const formattedTotals = totalCounts + .map((count) => String(count).padStart(width)) + .join(" "); + + process.stdout.write(`${formattedTotals} total\n`); +} From d2e7eae64e576a34eaad52bb2cf75c7ad873c409 Mon Sep 17 00:00:00 2001 From: XiaoQuark Date: Mon, 27 Jul 2026 21:17:47 +0100 Subject: [PATCH 5/6] refactor: wc implementation for easier readability --- implement-shell-tools/wc/wc.mjs | 126 +++++++++++++------------------- 1 file changed, 51 insertions(+), 75 deletions(-) diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs index 81befa607..1185e931d 100644 --- a/implement-shell-tools/wc/wc.mjs +++ b/implement-shell-tools/wc/wc.mjs @@ -27,116 +27,92 @@ if (positionals.length === 0) { process.exit(1); } -const results = []; +const fileCounts = []; -// read file and calculate counts for (const path of positionals) { - const content = fs.readFileSync(path, "utf-8"); + fileCounts.push(countFile(path)); +} - const lineCount = [...content].filter((char) => char === "\n").length; +const totalCounts = { + lineCount: 0, + wordCount: 0, + byteCount: 0, +}; - const trimmedContent = content.trim(); +for (const file of fileCounts) { + totalCounts.lineCount += file.lineCount; + totalCounts.wordCount += file.wordCount; + totalCounts.byteCount += file.byteCount; +} - const wordCount = - trimmedContent === "" ? 0 : trimmedContent.split(/\s+/).length; +// output formatting +const largestByteCount = + fileCounts.length > 1 ? totalCounts.byteCount : fileCounts[0].byteCount; - const byteCount = Buffer.byteLength(content); +const width = String(largestByteCount).length; - results.push({ - path, - lineCount, - wordCount, - byteCount, - }); +for (const file of fileCounts) { + const formattedCounts = formatCounts(file, width); + + process.stdout.write(`${formattedCounts} ${file.path}\n`); } -let totalLines = 0; -let totalWords = 0; -let totalBytes = 0; +if (fileCounts.length > 1) { + const formattedTotals = formatCounts(totalCounts, width); -// calculate total counts -for (const result of results) { - totalLines += result.lineCount; - totalWords += result.wordCount; - totalBytes += result.byteCount; + process.stdout.write(`${formattedTotals} total\n`); } -const allCounts = []; +function countFile(path) { + const content = fs.readFileSync(path, "utf-8"); -for (const result of results) { - const counts = getSelectedCounts( - result.lineCount, - result.wordCount, - result.byteCount, - ); + const lineCount = [...content].filter((char) => char === "\n").length; - allCounts.push(...counts); -} + const trimmedContent = content.trim(); -if (results.length > 1) { - const totalCounts = getSelectedCounts(totalLines, totalWords, totalBytes); + const wordCount = + trimmedContent === "" ? 0 : trimmedContent.split(/\s+/).length; - allCounts.push(...totalCounts); + const byteCount = Buffer.byteLength(content); + + return { + lineCount, + wordCount, + byteCount, + path, + }; } -// flags handling function function getSelectedCounts(lineCount, wordCount, byteCount) { - const counts = []; + const selectedCounts = []; if (values.lines) { - counts.push(lineCount); + selectedCounts.push(lineCount); } if (values.words) { - counts.push(wordCount); + selectedCounts.push(wordCount); } if (values.bytes) { - counts.push(byteCount); + selectedCounts.push(byteCount); } - if (counts.length === 0) { - counts.push(lineCount, wordCount, byteCount); + if (selectedCounts.length === 0) { + selectedCounts.push(lineCount, wordCount, byteCount); } - return counts; + return selectedCounts; } -// output formatting -let largestByteCount = 0; - -for (const result of results) { - if (result.byteCount > largestByteCount) { - largestByteCount = result.byteCount; - } -} - -if (results.length > 1 && totalBytes > largestByteCount) { - largestByteCount = totalBytes; -} - -const width = String(largestByteCount).length; - -for (const result of results) { - const counts = getSelectedCounts( - result.lineCount, - result.wordCount, - result.byteCount, +function formatCounts(counts, width) { + const selectedCounts = getSelectedCounts( + counts.lineCount, + counts.wordCount, + counts.byteCount, ); - const formattedCounts = counts + return selectedCounts .map((count) => String(count).padStart(width)) .join(" "); - - process.stdout.write(`${formattedCounts} ${result.path}\n`); -} - -if (results.length > 1) { - const totalCounts = getSelectedCounts(totalLines, totalWords, totalBytes); - - const formattedTotals = totalCounts - .map((count) => String(count).padStart(width)) - .join(" "); - - process.stdout.write(`${formattedTotals} total\n`); } From acb6980998f36ca6542772076bf2f81178db4631 Mon Sep 17 00:00:00 2001 From: XiaoQuark Date: Mon, 27 Jul 2026 21:25:00 +0100 Subject: [PATCH 6/6] feature: add --help support to ls implementation --- implement-shell-tools/ls/ls.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/implement-shell-tools/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs index 98fc45b3f..9817fd0fa 100644 --- a/implement-shell-tools/ls/ls.mjs +++ b/implement-shell-tools/ls/ls.mjs @@ -66,6 +66,22 @@ for (const arg of argv) { } } +if (options.help) { + process.stdout.write("Usage: ls [OPTION]... [FILE]...\n"); + process.stdout.write( + "List information about the FILEs (the current directory by default).\n\n", + ); + process.stdout.write("Options:\n"); + + for (const definition of Object.values(validFlags)) { + process.stdout.write( + ` ${definition.short}, ${definition.long}\t${definition.description}\n`, + ); + } + + process.exit(0); +} + if (paths.length === 0) { paths.push("."); }