diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 000000000..aa3c74d84 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,56 @@ +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 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("-")); + +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]; + + const isLastLine = i === lines.length - 1; + + if (isLastLine && line === "") { + break; + } + + 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 && isBlankLine) { + process.stdout.write("\n"); + } + } + } +} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 000000000..bced7ea05 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,59 @@ +import { promises as fs } from "node:fs"; +import process from "node:process"; + +const args = process.argv.slice(2); + +const showHidden = args.includes("-a"); + +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); +} + +const paths = args.filter((arg) => !arg.startsWith("-")); + +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/package.json b/implement-shell-tools/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 000000000..a22c8c262 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,85 @@ +import { promises as fs } from "node:fs"; +import process from "node:process"; + +const args = process.argv.slice(2); + +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 = expandedArgs.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); + +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 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, " ")); + return parts.join("") + " " + label; +} + +for (const { filePath, lines, words, bytes } of results) { + const counts = getCounts(lines, words, bytes); + process.stdout.write(formatLine(counts, filePath) + "\n"); +} + +if (results.length > 1) { + const totals = getCounts(totalLines, totalWords, totalBytes); + process.stdout.write(formatLine(totals, "total") + "\n"); +}