diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs new file mode 100644 index 00000000..64007159 --- /dev/null +++ b/implement-shell-tools/cat/cat.mjs @@ -0,0 +1,41 @@ +import { readFile, stat } from 'node:fs/promises'; +import { program } from 'commander'; + +program + .name('cat') + .description('Prints contents of files') + .option('-n, --number-all-lines', 'Number all output lines') + .option('-b, --number-non-blank', 'Number non-blank output lines') + .argument('', 'Files to read'); + +program.parse(); + +const { numberAllLines, numberNonBlank } = program.opts(); +const files = program.args; + +let lineNumber = 1; + +for (const file of files) { + try { + const fileStat = await stat(file); + if (fileStat.isDirectory()) { + console.error(`cat: ${file}: Is a directory`); + continue; + } + + const content = await readFile(file, 'utf-8'); + const lines = content.split('\n'); + + for (const line of lines) { + const shouldNumber = (numberAllLines && !numberNonBlank) || (numberNonBlank && line.trim() !== ''); + if (shouldNumber) { + console.log(`${lineNumber.toString().padStart(6)} ${line}`); + lineNumber++; + } else { + console.log(line); + } + } + } catch (err) { + console.error(`cat: ${file}: ${err.message}`); + } +} diff --git a/implement-shell-tools/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs new file mode 100644 index 00000000..fd9e95dc --- /dev/null +++ b/implement-shell-tools/ls/ls.mjs @@ -0,0 +1,37 @@ +import { program } from "commander"; +import { promises as fs } from "fs"; +import process from "process"; + +program + .name("ls") + .description("Lists the files in a directory") + .option("-1, --one", "One per line") + .option("-a, --all", "Include files starting with dot") + .argument("", "Directory to list"); + +program.parse(process.argv); + +const args = program.args; +const opts = program.opts(); + +if (args.length !== 1) { + console.error("Expected 1 argument"); + process.exit(1); +} + +try { + let files = await fs.readdir(args[0]); + if (!opts.all) { + files = files.filter(f => !f.startsWith('.')); + } + + files.sort(); + + if (opts.one) { + console.log(files.join('\n')); + } else { + console.log(files.join(' ')); + } +} catch (err) { + console.error(`ls: cannot access '${args[0]}': ${err.message}`); +} diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs new file mode 100644 index 00000000..54e844f7 --- /dev/null +++ b/implement-shell-tools/wc/wc.mjs @@ -0,0 +1,88 @@ +import { program } from "commander"; +import { readFileSync, existsSync } from "fs"; +import process from "process"; + +program + .name("wc") + .description("Count lines, words, and characters in files") + .argument("[files...]", "Files to process") + .option("-l, --lines", "Count lines") + .option("-w, --words", "Count words") + .option("-c, --chars", "Count characters (bytes)"); + +program.parse(process.argv); + +const options = program.opts(); +const files = program.args.length ? program.args : ["/dev/stdin"]; + +// Determine active counts +const activeCounts = { + lines: options.lines || (!options.words && !options.chars), + words: options.words || (!options.lines && !options.chars), + chars: options.chars || (!options.lines && !options.words), +}; + +function countFile(filePath) { + let content = ""; + try { + if (filePath === "/dev/stdin") { + content = readFileSync(process.stdin.fd, "utf8"); + } else { + if (!existsSync(filePath)) { + console.error(`wc: ${filePath}: No such file or directory`); + return null; + } + content = readFileSync(filePath, "utf8"); + } + } catch (error) { + console.error(`wc: ${filePath}: ${error.message}`); + return null; + } + + const lineCount = (content.match(/\n/g) || []).length; + const wordCount = content.trim().split(/\s+/).filter(Boolean).length; + const charCount = Buffer.byteLength(content, "utf8"); + + return { + file: filePath, + lines: activeCounts.lines ? lineCount : null, + words: activeCounts.words ? wordCount : null, + chars: activeCounts.chars ? charCount : null, + }; +} + +function formatCounts(result) { + const output = []; + if (result.lines !== null) output.push(result.lines.toString().padStart(8)); + if (result.words !== null) output.push(result.words.toString().padStart(8)); + if (result.chars !== null) output.push(result.chars.toString().padStart(8)); + return output.join(" "); +} + +const results = []; +let totalLines = 0, totalWords = 0, totalChars = 0; +const hasMultipleFiles = files.length > 1; + +for (const file of files) { + const result = countFile(file); + if (result) { + results.push(result); + if (result.lines !== null) totalLines += result.lines; + if (result.words !== null) totalWords += result.words; + if (result.chars !== null) totalChars += result.chars; + } +} + +// Print per-file results +results.forEach(result => console.log(`${formatCounts(result)} ${result.file}`)); + +// Print totals if more than one file +if (hasMultipleFiles && results.length > 0) { + const total = { + file: "total", + lines: activeCounts.lines ? totalLines : null, + words: activeCounts.words ? totalWords : null, + chars: activeCounts.chars ? totalChars : null, + }; + console.log(`${formatCounts(total)} total`); +}