|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +// A simple reimplementation of `cat` in NodeJS. |
| 4 | +// Supports: |
| 5 | +// node cat.js sample-files/1.txt |
| 6 | +// node cat.js -n sample-files/1.txt |
| 7 | +// node cat.js -b sample-files/3.txt |
| 8 | +// node cat.js sample-files/*.txt |
| 9 | +// |
| 10 | +// -n : number all lines |
| 11 | +// -b : number non-empty lines |
| 12 | + |
| 13 | +const fs = require("fs"); |
| 14 | +const path = require("path"); |
| 15 | + |
| 16 | +// -------- argument parsing -------- |
| 17 | +const args = process.argv.slice(2); |
| 18 | + |
| 19 | +if (args.length === 0) { |
| 20 | + console.error("Usage: node cat.js [-n | -b] <file> [<file> ...]"); |
| 21 | + process.exit(1); |
| 22 | +} |
| 23 | + |
| 24 | +let numberAll = false; |
| 25 | +let numberNonBlank = false; |
| 26 | +let fileArgs = []; |
| 27 | + |
| 28 | +// Very small argument parser: |
| 29 | +// We only care about -n or -b as the FIRST arg (like the coursework examples). |
| 30 | +if (args[0] === "-n") { |
| 31 | + numberAll = true; |
| 32 | + fileArgs = args.slice(1); |
| 33 | +} else if (args[0] === "-b") { |
| 34 | + numberNonBlank = true; |
| 35 | + fileArgs = args.slice(1); |
| 36 | +} else { |
| 37 | + fileArgs = args; |
| 38 | +} |
| 39 | + |
| 40 | +if (fileArgs.length === 0) { |
| 41 | + console.error("cat.js: no input files"); |
| 42 | + process.exit(1); |
| 43 | +} |
| 44 | + |
| 45 | +// -------- helper functions -------- |
| 46 | + |
| 47 | +/** |
| 48 | + * Format a single line with the correct line number, if needed. |
| 49 | + * |
| 50 | + * @param {string} line - line text (without the trailing newline) |
| 51 | + * @param {number} currentLineNumber - global line counter |
| 52 | + * @param {boolean} numberAll - true if -n |
| 53 | + * @param {boolean} numberNonBlank - true if -b |
| 54 | + * @returns {{ text: string, nextLineNumber: number }} |
| 55 | + */ |
| 56 | +function formatLine(line, currentLineNumber, numberAll, numberNonBlank) { |
| 57 | + let output = ""; |
| 58 | + let nextLineNumber = currentLineNumber; |
| 59 | + |
| 60 | + const isBlank = line === ""; |
| 61 | + |
| 62 | + if (numberAll) { |
| 63 | + // Always number every line |
| 64 | + output = |
| 65 | + currentLineNumber.toString().padStart(6, " ") + "\t" + line; |
| 66 | + nextLineNumber++; |
| 67 | + } else if (numberNonBlank) { |
| 68 | + // Number only non-blank lines |
| 69 | + if (isBlank) { |
| 70 | + output = line; // No number, just the blank line |
| 71 | + } else { |
| 72 | + output = |
| 73 | + currentLineNumber.toString().padStart(6, " ") + "\t" + line; |
| 74 | + nextLineNumber++; |
| 75 | + } |
| 76 | + } else { |
| 77 | + // No numbering |
| 78 | + output = line; |
| 79 | + } |
| 80 | + |
| 81 | + return { text: output, nextLineNumber }; |
| 82 | +} |
| 83 | + |
| 84 | +/** |
| 85 | + * Print a file to stdout according to the options. |
| 86 | + * |
| 87 | + * @param {string} filePath |
| 88 | + * @param {number} startLineNumber |
| 89 | + * @returns {number} next line number |
| 90 | + */ |
| 91 | +function printFile(filePath, startLineNumber) { |
| 92 | + let content; |
| 93 | + |
| 94 | + try { |
| 95 | + content = fs.readFileSync(filePath, "utf8"); |
| 96 | + } catch (err) { |
| 97 | + console.error(`cat.js: ${filePath}: ${err.message}`); |
| 98 | + return startLineNumber; |
| 99 | + } |
| 100 | + |
| 101 | + const lines = content.split("\n"); |
| 102 | + let lineNumber = startLineNumber; |
| 103 | + |
| 104 | + for (let i = 0; i < lines.length; i++) { |
| 105 | + const line = lines[i]; |
| 106 | + const { text, nextLineNumber } = formatLine( |
| 107 | + line, |
| 108 | + lineNumber, |
| 109 | + numberAll, |
| 110 | + numberNonBlank |
| 111 | + ); |
| 112 | + lineNumber = nextLineNumber; |
| 113 | + |
| 114 | + // Re-add the newline we lost when splitting |
| 115 | + process.stdout.write(text); |
| 116 | + |
| 117 | + // Avoid adding an extra newline at very end if the file |
| 118 | + // doesn't end with \n — Node's split keeps the last segment. |
| 119 | + if (i < lines.length - 1 || content.endsWith("\n")) { |
| 120 | + process.stdout.write("\n"); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + return lineNumber; |
| 125 | +} |
| 126 | + |
| 127 | +// -------- main execution -------- |
| 128 | + |
| 129 | +let globalLineNumber = 1; |
| 130 | + |
| 131 | +for (const file of fileArgs) { |
| 132 | + // Shell expands *.txt, so here we just get each file path. |
| 133 | + const resolvedPath = path.resolve(file); |
| 134 | + globalLineNumber = printFile(resolvedPath, globalLineNumber); |
| 135 | +} |
0 commit comments