diff --git a/implement-shell-tools/cat/cat.js b/implement-shell-tools/cat/cat.js new file mode 100644 index 00000000..8b417568 --- /dev/null +++ b/implement-shell-tools/cat/cat.js @@ -0,0 +1,56 @@ +import { program } from "commander"; +import process from "node:process"; +import { promises as fs } from "node:fs"; +import glob from "glob"; + +program + .name("cat") + .description("print the content of file") + .option("-n, --line-numbers", "Number the output lines, starting at 1") + .option( + "-b, --number-nonblank", + "Number non-empty output lines, overrides -n" + ) + .argument("", "The file path(s) to process"); // to support multiple file +program.parse(); + +const argv = program.args.flatMap((path) => glob.sync(path)); + +const options = program.opts(); +if (argv.length === 0) { + console.error(`No file paths provided`); // to support more files + process.exit(1); +} + +let lineCounter = 1; +function printLine(line, number = false) { + if (number && line.trim() !== "") { + const lineNumber = String(lineCounter++).padStart(6, " "); + console.log(`${lineNumber}\t${line}`); + } else { + console.log(line); + } +} +// Helper function to print totals for multiple files +function printTotal(total, label = "total") { + console.log(`${total} ${label}`); +} +for (const path of argv) { + try { + const content = await fs.readFile(path, "utf-8"); + const lines = content.split(/\r?\n/); + if (lines.length && lines[lines.length - 1] === "") { + // Remove trailing empty line if it's just from the final newline + lines.pop(); + } + // Determine if we need to number lines + const numberLines = options.numberNonblank || options.lineNumbers; + + lines.forEach((line) => printLine(line, numberLines)); + + // Add a newline if the file didn't end with one + if (!content.endsWith("\n")) process.stdout.write("\n"); + } catch (err) { + console.error(`cat: ${path}: ${err.message}`); + } +} diff --git a/implement-shell-tools/ls/ls.js b/implement-shell-tools/ls/ls.js new file mode 100644 index 00000000..d90b57b5 --- /dev/null +++ b/implement-shell-tools/ls/ls.js @@ -0,0 +1,35 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; + +program + .name("ls") + .description("Displays files in a directory") + .option("-1, --one", "Display each file on a new line")// for listing one file per line + .option("-a, --all", "Include hidden files") + .argument("[filepath]", "Directory to list files from (default: current)") + +await program.parseAsync(); + +const opts = program.opts(); +const args = program.args; + +const path = args[0] || "."; //Use current directory if no path provided +let files ; +try { + files = await fs.readdir(path); +}catch (err) { + console.error(`ls: cannot access '${path}': ${err.message}`); + process.exit(1); + +} + +if (!opts.all) { + files = files.filter(file => !file.startsWith("."));// if path not provided use current directory +} + +if (opts.one) { + files.forEach(file => console.log(file)); +} else { + console.log(files.join(" ")); +} + diff --git a/implement-shell-tools/package-lock.json b/implement-shell-tools/package-lock.json new file mode 100644 index 00000000..520e1d48 --- /dev/null +++ b/implement-shell-tools/package-lock.json @@ -0,0 +1,107 @@ +{ + "name": "implement-shell-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "commander": "^14.0.0", + "glob": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/implement-shell-tools/package.json b/implement-shell-tools/package.json new file mode 100644 index 00000000..f7d3ca5a --- /dev/null +++ b/implement-shell-tools/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "commander": "^14.0.0", + "glob": "^8.1.0" + } +} diff --git a/implement-shell-tools/wc/wc.js b/implement-shell-tools/wc/wc.js new file mode 100644 index 00000000..0b335d42 --- /dev/null +++ b/implement-shell-tools/wc/wc.js @@ -0,0 +1,61 @@ +import { program } from "commander"; +import { promises as fs } from "node:fs"; +import pkg from "glob"; +const { glob } = pkg; + +program + .name("wc") + .description("Count lines, words, and bytes in files") + .option("-l, --lines", "Only print line counts") + .option("-w, --words", "Only print word counts") + .option("-c, --bytes", "Only print byte counts") + .argument("", "One or more files to process"); +await program.parseAsync(); + +const files = program.args.flatMap((path) => glob.sync(path)); +const options = program.opts(); + +function countContent(content) { + const lines = (content.match(/\n/g) || []).length; + const words = content.trim().split(/\s+/).filter(Boolean).length; + const bytes = Buffer.byteLength(content, "utf-8"); + return { lines, words, bytes }; +} +// Helper to print counts based on options +function printCounts({ lines, words, bytes }, label) { + if (options.lines) { + console.log(`${lines} ${label}`); + } else if (options.words) { + console.log(`${words} ${label}`); + } else if (options.bytes) { + console.log(`${bytes} ${label}`); + } else { + console.log(`${lines} ${words} ${bytes} ${label}`); + } +} + +let totalLines = 0; +let totalWords = 0; +let totalBytes = 0; + +for (const file of files) { + try { + const content = await fs.readFile(file, "utf-8"); + const counts = countContent(content); + + printCounts(counts, file); + + totalLines += counts.lines; + totalWords += counts.words; + totalBytes += counts.bytes; + } catch (err) { + console.error(`wc: ${file}: ${err.message}`); + } +} + +if (files.length > 1) { + printCounts( + { lines: totalLines, words: totalWords, bytes: totalBytes }, + "total" + ); +}