diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 00000000..e9db974a --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +// A simple reimplementation of `cat` in NodeJS. +// Supports: +// node cat.js sample-files/1.txt +// node cat.js -n sample-files/1.txt +// node cat.js -b sample-files/3.txt +// node cat.js sample-files/*.txt +// +// -n : number all lines +// -b : number non-empty lines + +const fs = require("fs"); +const path = require("path"); + +// -------- argument parsing -------- +const args = process.argv.slice(2); + +if (args.length === 0) { + console.error("Usage: node cat.js [-n | -b] [ ...]"); + process.exit(1); +} + +let numberAll = false; +let numberNonBlank = false; +let fileArgs = []; + +// Very small argument parser: +// We only care about -n or -b as the FIRST arg (like the coursework examples). +if (args[0] === "-n") { + numberAll = true; + fileArgs = args.slice(1); +} else if (args[0] === "-b") { + numberNonBlank = true; + fileArgs = args.slice(1); +} else { + fileArgs = args; +} + +if (fileArgs.length === 0) { + console.error("cat.js: no input files"); + process.exit(1); +} + +// -------- helper functions -------- + +/** + * Format a single line with the correct line number, if needed. + * + * @param {string} line - line text (without the trailing newline) + * @param {number} currentLineNumber - global line counter + * @param {boolean} numberAll - true if -n + * @param {boolean} numberNonBlank - true if -b + * @returns {{ text: string, nextLineNumber: number }} + */ +function formatLine(line, currentLineNumber, numberAll, numberNonBlank) { + let output = ""; + let nextLineNumber = currentLineNumber; + + const isBlank = line === ""; + + if (numberAll) { + // Always number every line + output = + currentLineNumber.toString().padStart(6, " ") + "\t" + line; + nextLineNumber++; + } else if (numberNonBlank) { + // Number only non-blank lines + if (isBlank) { + output = line; // No number, just the blank line + } else { + output = + currentLineNumber.toString().padStart(6, " ") + "\t" + line; + nextLineNumber++; + } + } else { + // No numbering + output = line; + } + + return { text: output, nextLineNumber }; +} + +/** + * Print a file to stdout according to the options. + * + * @param {string} filePath + * @param {number} startLineNumber + * @returns {number} next line number + */ +function printFile(filePath, startLineNumber) { + let content; + + try { + content = fs.readFileSync(filePath, "utf8"); + } catch (err) { + console.error(`cat.js: ${filePath}: ${err.message}`); + return startLineNumber; + } + + const lines = content.split("\n"); + let lineNumber = startLineNumber; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const { text, nextLineNumber } = formatLine( + line, + lineNumber, + numberAll, + numberNonBlank + ); + lineNumber = nextLineNumber; + + // Re-add the newline we lost when splitting + process.stdout.write(text); + + // Avoid adding an extra newline at very end if the file + // doesn't end with \n — Node's split keeps the last segment. + if (i < lines.length - 1 || content.endsWith("\n")) { + process.stdout.write("\n"); + } + } + + return lineNumber; +} + +// -------- main execution -------- + +let globalLineNumber = 1; + +for (const file of fileArgs) { + // Shell expands *.txt, so here we just get each file path. + const resolvedPath = path.resolve(file); + globalLineNumber = printFile(resolvedPath, globalLineNumber); +} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 00000000..ef8ee870 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +// -------- args parsing -------- +const args = process.argv.slice(2); + +let onePerLine = false; // -1 +let showAll = false; // -a +let targets = []; + +for (const arg of args) { + if (arg === "-1") onePerLine = true; + else if (arg === "-a") showAll = true; + else targets.push(arg); +} + +// Coursework only tests -1 variants, so enforce it clearly +if (!onePerLine) { + console.error("Usage: node ls.js -1 [-a] [path]"); + process.exit(1); +} + +if (targets.length === 0) targets = ["."]; +if (targets.length > 1) { + console.error("ls.js: only one path is supported in this exercise"); + process.exit(1); +} + +const target = targets[0]; + +// -------- helpers -------- +function sortLikeLs(names) { + return names.sort((a, b) => a.localeCompare(b)); +} + +function listDir(dirPath) { + let entries; + try { + entries = fs.readdirSync(dirPath, { withFileTypes: false }); + } catch (err) { + console.error(`ls.js: cannot access '${dirPath}': ${err.message}`); + process.exit(1); + } + + // readdirSync does NOT include "." and ".." — ls -a does. + if (showAll) { + // keep dotfiles + add . and .. + entries = [".", "..", ...entries]; + } else { + // hide dotfiles + entries = entries.filter((name) => !name.startsWith(".")); + } + + entries = sortLikeLs(entries); + + // -1 => one per line + for (const name of entries) { + process.stdout.write(name + "\n"); + } +} + +function listFile(filePath) { + // ls -1 file => prints the file name + process.stdout.write(path.basename(filePath) + "\n"); +} + +// -------- main -------- +let stat; +try { + stat = fs.statSync(target); +} catch (err) { + console.error(`ls.js: cannot access '${target}': ${err.message}`); + process.exit(1); +} + +if (stat.isDirectory()) listDir(target); +else listFile(target); diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 00000000..be1d17a4 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +// -------- argument parsing -------- +const args = process.argv.slice(2); + +let countLines = false; +let countWords = false; +let countBytes = false; +let files = []; + +// flags can appear in any order +for (const arg of args) { + if (arg === "-l") countLines = true; + else if (arg === "-w") countWords = true; + else if (arg === "-c") countBytes = true; + else files.push(arg); +} + +if (files.length === 0) { + console.error("Usage: wc [-l] [-w] [-c] [file...]"); + process.exit(1); +} + +// If no flags → behave like plain `wc` +if (!countLines && !countWords && !countBytes) { + countLines = true; + countWords = true; + countBytes = true; +} + +// -------- helpers -------- +function countFile(filePath) { + let content; + let buffer; + + try { + buffer = fs.readFileSync(filePath); + content = buffer.toString("utf8"); + } catch (err) { + console.error(`wc.js: ${filePath}: ${err.message}`); + return null; + } + + const lines = content.split("\n").length - 1; + const words = content.trim() === "" + ? 0 + : content.trim().split(/\s+/).length; + const bytes = buffer.length; + + return { lines, words, bytes }; +} + +function formatOutput(counts, fileName) { + const parts = []; + + if (countLines) parts.push(counts.lines.toString().padStart(7, " ")); + if (countWords) parts.push(counts.words.toString().padStart(7, " ")); + if (countBytes) parts.push(counts.bytes.toString().padStart(7, " ")); + + parts.push(" " + fileName); + return parts.join(""); +} + +// -------- main -------- +let total = { lines: 0, words: 0, bytes: 0 }; +let validFileCount = 0; + +for (const file of files) { + const counts = countFile(file); + if (!counts) continue; + + validFileCount++; + + total.lines += counts.lines; + total.words += counts.words; + total.bytes += counts.bytes; + + process.stdout.write(formatOutput(counts, file) + "\n"); +} + +// Print total if multiple files +if (validFileCount > 1) { + process.stdout.write(formatOutput(total, "total") + "\n"); +}