diff --git a/implement-shell-tools/cat/script-cat.js b/implement-shell-tools/cat/script-cat.js new file mode 100755 index 00000000..a255ebc5 --- /dev/null +++ b/implement-shell-tools/cat/script-cat.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +// Setup CLI +program + .name("cat") + .description("Concatenate files and print on the standard output") + .option("-n, --number", "number all output lines") + .option("-b, --number-nonblank", "number nonempty output lines") + .argument("", "file(s) to read"); + +program.parse(); + +const files = program.args; +const { number, numberNonblank } = program.opts(); + +// Validate input +if (files.length === 0) { + console.error("cat: missing file operand"); + process.exit(1); +} + +let lineNumber = 1; + +// Helper to print lines with optional numbering +function printLine(line, shouldNumber) { + if (shouldNumber) { + console.log(`${String(lineNumber).padStart(6)}\t${line}`); + lineNumber++; + } else { + console.log(line); + } +} + +for (const file of files) { + let content; + try { + content = await fs.readFile(file, "utf8"); + } catch (err) { + console.error(`cat: ${file}: ${err.message}`); + continue; + } + + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Avoid printing an extra line if file ends with \n + if (i === lines.length - 1 && line === "") { + break; + } + + const isBlank = line.trim() === ""; + + if (numberNonblank) { + printLine(line, !isBlank); + } else if (number) { + printLine(line, true); + } else { + printLine(line, false); + } + } +} diff --git a/implement-shell-tools/ls/script-ls.js b/implement-shell-tools/ls/script-ls.js new file mode 100644 index 00000000..c474b45c --- /dev/null +++ b/implement-shell-tools/ls/script-ls.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import { program } from "commander"; +import fs from "fs"; +import path from "path"; + +// Define CLI +program + .name("ls-clone") + .description("A simple implementation of ls") + .option("-1", "list one file per line") + .option("-a", "include hidden files") + .argument("[dirs...]", "directories to list", "."); // default is current dir + +program.parse(); + +const options = program.opts(); +const dirs = program.args.length ? program.args : ["."]; +const onePerLine = options["1"]; +const showAll = options.a; + +for (const dir of dirs) { + let files; + try { + files = fs.readdirSync(dir); + } catch (err) { + console.error(`ls-clone: cannot access '${dir}': No such file or directory`); + continue; + } + + if (!showAll) { + files = files.filter(name => !name.startsWith(".")); + } + + // Output + if (onePerLine) { + files.forEach(f => console.log(f)); + } else { + console.log(files.join(" ")); + } +} diff --git a/implement-shell-tools/wc/script-wc.js b/implement-shell-tools/wc/script-wc.js new file mode 100755 index 00000000..c3fbfc81 --- /dev/null +++ b/implement-shell-tools/wc/script-wc.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import process from "node:process"; + +// Setup CLI +program + .name("wc-clone") + .description("A simplified implementation of the wc command") + .option("-l", "count lines") + .option("-w", "count words") + .option("-c", "count bytes") + .argument("[files...]", "files to process"); + +program.parse(); + +const options = program.opts(); +const files = program.args; + +if (files.length === 0) { + console.error("Please provide at least one file."); + process.exit(1); +} + +// Count lines, words, bytes +function countContent(content) { + const lines = content.split("\n").length; + const words = content.trim().split(/\s+/).filter(Boolean).length; + const bytes = Buffer.byteLength(content, "utf-8"); + return { lines, words, bytes }; +} + +// Format output consistently +function formatOutput(counts, label = "") { + const showAll = !options.l && !options.w && !options.c; + const parts = []; + + if (options.l || showAll) parts.push(counts.lines.toString().padStart(8)); + if (options.w || showAll) parts.push(counts.words.toString().padStart(8)); + if (options.c || showAll) parts.push(counts.bytes.toString().padStart(8)); + + if (label) parts.push(label); + + return parts.join(" "); +} + +(async () => { + let total = { lines: 0, words: 0, bytes: 0 }; + const multipleFiles = files.length > 1; + + for (const file of files) { + let content; + try { + content = await fs.readFile(file, "utf-8"); + } catch { + console.error(`wc-clone: cannot open '${file}': No such file`); + continue; + } + + const counts = countContent(content); + + total.lines += counts.lines; + total.words += counts.words; + total.bytes += counts.bytes; + + console.log(formatOutput(counts, file)); + } + + if (multipleFiles) { + console.log(formatOutput(total, "total")); + } +})();