From 57a3c2ab4f2d486331273bb645bd96cb328aa7fc Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Tue, 29 Jul 2025 20:28:45 +0200 Subject: [PATCH 1/5] added code for ls exercise --- implement-shell-tools/ls/ls.mjs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 implement-shell-tools/ls/ls.mjs diff --git a/implement-shell-tools/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs new file mode 100644 index 00000000..daabfec0 --- /dev/null +++ b/implement-shell-tools/ls/ls.mjs @@ -0,0 +1,24 @@ +import { program } from "commander" +import process from "node:process" +import { promises as fs } from "node:fs" +import { readdir } from 'node:fs/promises' + +program + .name("ls") + .description("Lists the files in a directory") + .option("-1, --one", "One per line") + .option("-a", "Include files starting with dot") + .argument("filepath") +program.parse(process.argv) + +const argv = program.args +const opts = program.opts() + +if (argv.length != 1){ + console.error("Expected 1 argument") + process.exit(1) +} + +const content = await fs.readdir(argv[0]) + +console.log(content.join(opts.one ? "\n": " ")) From b94590210025fc364acceb2393bc39e186a3cbf2 Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Wed, 30 Jul 2025 08:38:14 +0200 Subject: [PATCH 2/5] added code for the cat file exercises --- implement-shell-tools/cat/cat.mjs | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 implement-shell-tools/cat/cat.mjs diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs new file mode 100644 index 00000000..79d702a8 --- /dev/null +++ b/implement-shell-tools/cat/cat.mjs @@ -0,0 +1,40 @@ +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, numNotBlank } = 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 && !numNotBlank) || (numNotBlank && line.trim() !== ''); + + if (shouldNumber) { + console.log(`${lineNumber.toString().padStart(6)} ${line}`); + lineNumber++; + } else { + console.log(line); + } + } + } catch (err) { + console.error(`cat: ${file}: ${err.message}`); + } +} \ No newline at end of file From dae3b6259c882bdf49a6a1a6777b7549ec04f803 Mon Sep 17 00:00:00 2001 From: Luke-Manyamazi Date: Wed, 30 Jul 2025 09:59:57 +0200 Subject: [PATCH 3/5] added code for the wc exercise --- implement-shell-tools/wc/wc.mjs | 89 +++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 implement-shell-tools/wc/wc.mjs diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs new file mode 100644 index 00000000..6fdf0280 --- /dev/null +++ b/implement-shell-tools/wc/wc.mjs @@ -0,0 +1,89 @@ +import { program } from "commander"; +import process from "node:process"; +import { readFileSync, existsSync } from "node:fs"; + +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"]; + +function countFile(filePath, options) { + 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; + } + + if (typeof content !== 'string') { + content = ""; + } + + 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: options.lines || (!options.words && !options.chars) ? lineCount : null, + words: options.words || (!options.lines && !options.chars) ? wordCount : null, + chars: options.chars || (!options.lines && !options.words) ? charCount : null, + }; +} + +const results = []; +let totalLines = 0; +let totalWords = 0; +let totalChars = 0; +const hasMultipleFiles = files.length > 1; + +files.forEach(file => { + const result = countFile(file, options); + 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; + } +}); + +results.forEach(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)); + console.log(output.join(" "), result.file); +}); + +if (hasMultipleFiles && results.length > 0) { + const totalOutput = []; + if (options.lines || (!options.words && !options.chars)) { + totalOutput.push(totalLines.toString().padStart(8)); + } + if (options.words || (!options.lines && !options.chars)) { + totalOutput.push(totalWords.toString().padStart(8)); + } + if (options.chars || (!options.lines && !options.words)) { + totalOutput.push(totalChars.toString().padStart(8)); + } + console.log(totalOutput.join(" "), "total"); +} \ No newline at end of file From 6da6c6bb96e2adbd6aef3237f21fb29a3a09cf3b Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Mon, 13 Oct 2025 21:24:24 +0200 Subject: [PATCH 4/5] Refactor shell tools: cat, ls, wc - cat.mjs: - Fixed -b option to number only non-blank lines. - Unified line number padding to avoid repetition. - Added error handling for directories and missing files. - ls.mjs: - Correctly implemented -a option to include hidden files. - Validates single directory argument. - Sorted output and handled one-column listing. - wc.mjs: - Centralized line, word, and character counting in countFile(). - Defaults to counting all metrics if no options are given. - Added support for multiple files with totals. - Improved error handling for missing files and directories. Overall: - Removed duplication, clarified logic, and improved alignment with standard Unix behavior. --- implement-shell-tools/cat/cat.mjs | 11 ++++---- implement-shell-tools/ls/ls.mjs | 47 ++++++++++++++++++++----------- implement-shell-tools/wc/wc.mjs | 36 ++++++++--------------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/implement-shell-tools/cat/cat.mjs b/implement-shell-tools/cat/cat.mjs index 79d702a8..64007159 100644 --- a/implement-shell-tools/cat/cat.mjs +++ b/implement-shell-tools/cat/cat.mjs @@ -6,10 +6,11 @@ program .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') + .argument('', 'Files to read'); + program.parse(); -const { numberAllLines, numNotBlank } = program.opts(); +const { numberAllLines, numberNonBlank } = program.opts(); const files = program.args; let lineNumber = 1; @@ -21,12 +22,12 @@ for (const file of files) { 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 && !numNotBlank) || (numNotBlank && line.trim() !== ''); - + const shouldNumber = (numberAllLines && !numberNonBlank) || (numberNonBlank && line.trim() !== ''); if (shouldNumber) { console.log(`${lineNumber.toString().padStart(6)} ${line}`); lineNumber++; @@ -37,4 +38,4 @@ for (const file of files) { } catch (err) { console.error(`cat: ${file}: ${err.message}`); } -} \ No newline at end of file +} diff --git a/implement-shell-tools/ls/ls.mjs b/implement-shell-tools/ls/ls.mjs index daabfec0..fd9e95dc 100644 --- a/implement-shell-tools/ls/ls.mjs +++ b/implement-shell-tools/ls/ls.mjs @@ -1,24 +1,37 @@ -import { program } from "commander" -import process from "node:process" -import { promises as fs } from "node:fs" -import { readdir } from 'node:fs/promises' +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", "Include files starting with dot") - .argument("filepath") -program.parse(process.argv) + .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"); -const argv = program.args -const opts = program.opts() +program.parse(process.argv); -if (argv.length != 1){ - console.error("Expected 1 argument") - process.exit(1) +const args = program.args; +const opts = program.opts(); + +if (args.length !== 1) { + console.error("Expected 1 argument"); + process.exit(1); } -const content = await fs.readdir(argv[0]) +try { + let files = await fs.readdir(args[0]); + if (!opts.all) { + files = files.filter(f => !f.startsWith('.')); + } + + files.sort(); -console.log(content.join(opts.one ? "\n": " ")) + 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 index 6fdf0280..43ecba8a 100644 --- a/implement-shell-tools/wc/wc.mjs +++ b/implement-shell-tools/wc/wc.mjs @@ -1,6 +1,6 @@ import { program } from "commander"; -import process from "node:process"; -import { readFileSync, existsSync } from "node:fs"; +import { readFileSync, existsSync } from "fs"; +import process from "process"; program .name("wc") @@ -8,7 +8,8 @@ program .argument("[files...]", "Files to process") .option("-l, --lines", "Count lines") .option("-w, --words", "Count words") - .option("-c, --chars", "Count characters (bytes)") + .option("-c, --chars", "Count characters (bytes)"); + program.parse(process.argv); const options = program.opts(); @@ -31,14 +32,8 @@ function countFile(filePath, options) { return null; } - if (typeof content !== 'string') { - content = ""; - } - const lineCount = (content.match(/\n/g) || []).length; - const wordCount = content.trim().split(/\s+/).filter(Boolean).length; - const charCount = Buffer.byteLength(content, "utf8"); return { @@ -50,21 +45,18 @@ function countFile(filePath, options) { } const results = []; -let totalLines = 0; -let totalWords = 0; -let totalChars = 0; +let totalLines = 0, totalWords = 0, totalChars = 0; const hasMultipleFiles = files.length > 1; -files.forEach(file => { +for (const file of files) { const result = countFile(file, options); 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; } -}); +} results.forEach(result => { const output = []; @@ -76,14 +68,8 @@ results.forEach(result => { if (hasMultipleFiles && results.length > 0) { const totalOutput = []; - if (options.lines || (!options.words && !options.chars)) { - totalOutput.push(totalLines.toString().padStart(8)); - } - if (options.words || (!options.lines && !options.chars)) { - totalOutput.push(totalWords.toString().padStart(8)); - } - if (options.chars || (!options.lines && !options.words)) { - totalOutput.push(totalChars.toString().padStart(8)); - } + if (options.lines || (!options.words && !options.chars)) totalOutput.push(totalLines.toString().padStart(8)); + if (options.words || (!options.lines && !options.chars)) totalOutput.push(totalWords.toString().padStart(8)); + if (options.chars || (!options.lines && !options.words)) totalOutput.push(totalChars.toString().padStart(8)); console.log(totalOutput.join(" "), "total"); -} \ No newline at end of file +} From c096a0b77d641ff228241f2003999882872d3f66 Mon Sep 17 00:00:00 2001 From: Luke Manyamazi Date: Tue, 14 Oct 2025 15:38:09 +0200 Subject: [PATCH 5/5] Refactor wc output formatting - Abstract duplicated code for printing counts into `formatCounts()` helper - Precompute active count options in `activeCounts` to remove repeated ternaries - Simplify per-file and total output logic for readability and maintainability --- implement-shell-tools/wc/wc.mjs | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/implement-shell-tools/wc/wc.mjs b/implement-shell-tools/wc/wc.mjs index 43ecba8a..54e844f7 100644 --- a/implement-shell-tools/wc/wc.mjs +++ b/implement-shell-tools/wc/wc.mjs @@ -15,7 +15,14 @@ program.parse(process.argv); const options = program.opts(); const files = program.args.length ? program.args : ["/dev/stdin"]; -function countFile(filePath, options) { +// 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") { @@ -38,18 +45,26 @@ function countFile(filePath, options) { return { file: filePath, - lines: options.lines || (!options.words && !options.chars) ? lineCount : null, - words: options.words || (!options.lines && !options.chars) ? wordCount : null, - chars: options.chars || (!options.lines && !options.words) ? charCount : null, + 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, options); + const result = countFile(file); if (result) { results.push(result); if (result.lines !== null) totalLines += result.lines; @@ -58,18 +73,16 @@ for (const file of files) { } } -results.forEach(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)); - console.log(output.join(" "), result.file); -}); +// 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 totalOutput = []; - if (options.lines || (!options.words && !options.chars)) totalOutput.push(totalLines.toString().padStart(8)); - if (options.words || (!options.lines && !options.chars)) totalOutput.push(totalWords.toString().padStart(8)); - if (options.chars || (!options.lines && !options.words)) totalOutput.push(totalChars.toString().padStart(8)); - console.log(totalOutput.join(" "), "total"); + const total = { + file: "total", + lines: activeCounts.lines ? totalLines : null, + words: activeCounts.words ? totalWords : null, + chars: activeCounts.chars ? totalChars : null, + }; + console.log(`${formatCounts(total)} total`); }