diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs new file mode 100644 index 000000000..431dfc1b0 --- /dev/null +++ b/implement-shell-tools/cat/cat.mjs @@ -0,0 +1,53 @@ +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") + .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"); + 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/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs new file mode 100644 index 000000000..9817fd0fa --- /dev/null +++ b/implement-shell-tools/ls/ls.mjs @@ -0,0 +1,126 @@ +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 (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("."); +} + +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`); +} 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" + } +} diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs new file mode 100644 index 000000000..1185e931d --- /dev/null +++ b/implement-shell-tools/wc/wc.mjs @@ -0,0 +1,118 @@ +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 fileCounts = []; + +for (const path of positionals) { + fileCounts.push(countFile(path)); +} + +const totalCounts = { + lineCount: 0, + wordCount: 0, + byteCount: 0, +}; + +for (const file of fileCounts) { + totalCounts.lineCount += file.lineCount; + totalCounts.wordCount += file.wordCount; + totalCounts.byteCount += file.byteCount; +} + +// output formatting +const largestByteCount = + fileCounts.length > 1 ? totalCounts.byteCount : fileCounts[0].byteCount; + +const width = String(largestByteCount).length; + +for (const file of fileCounts) { + const formattedCounts = formatCounts(file, width); + + process.stdout.write(`${formattedCounts} ${file.path}\n`); +} + +if (fileCounts.length > 1) { + const formattedTotals = formatCounts(totalCounts, width); + + process.stdout.write(`${formattedTotals} total\n`); +} + +function countFile(path) { + 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); + + return { + lineCount, + wordCount, + byteCount, + path, + }; +} + +function getSelectedCounts(lineCount, wordCount, byteCount) { + const selectedCounts = []; + + if (values.lines) { + selectedCounts.push(lineCount); + } + + if (values.words) { + selectedCounts.push(wordCount); + } + + if (values.bytes) { + selectedCounts.push(byteCount); + } + + if (selectedCounts.length === 0) { + selectedCounts.push(lineCount, wordCount, byteCount); + } + + return selectedCounts; +} + +function formatCounts(counts, width) { + const selectedCounts = getSelectedCounts( + counts.lineCount, + counts.wordCount, + counts.byteCount, + ); + + return selectedCounts + .map((count) => String(count).padStart(width)) + .join(" "); +}