Skip to content
10 changes: 5 additions & 5 deletions implement-shell-tools/cat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Your task is to implement your own version of `cat`.

It must act the same as `cat` would, if run from the directory containing this README.md file, for the following command lines:

* `cat sample-files/1.txt`
* `cat -n sample-files/1.txt`
* `cat sample-files/*.txt`
* `cat -n sample-files/*.txt`
* `cat -b sample-files/3.txt`
- `cat sample-files/1.txt`
- `cat -n sample-files/1.txt`
- `cat sample-files/*.txt`
- `cat -n sample-files/*.txt`
- `cat -b sample-files/3.txt`

Matching any additional behaviours or flags are optional stretch goals.

Expand Down
53 changes: 53 additions & 0 deletions implement-shell-tools/cat/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import process from "node:process";
import { promises as fs } from "node:fs";
import { program } from "commander";

program
.option("-n, --number", "number all output lines")
.option("-b, --number-nonblank", "number only non-empty lines")
.arguments("<files...>")
.parse();

const cliOptions = program.opts();
const filePathsToRead = program.args;

async function readAndOutputFiles() {
try {
const fileContents = await Promise.all(
filePathsToRead.map((filePath) => fs.readFile(filePath, "utf-8")),
);
const concatenatedContent = fileContents.join("");

if (cliOptions.number) {
// apply -n logic: number all lines
const contentLines = concatenatedContent.split("\n");
const numberedOutput = contentLines
.map((line, index) => {
return `${String(index + 1).padStart(6)} ${line}`;
})
.join("\n");
process.stdout.write(numberedOutput);
} else if (cliOptions.numberNonblank) {
// apply -b logic: number only non-empty lines
const contentLines = concatenatedContent.split("\n");
let nonblankLineNumber = 0;
const numberedOutput = contentLines
.map((line) => {
if (line.trim() === "") {
return line;
}
nonblankLineNumber++;
return `${String(nonblankLineNumber).padStart(6)} ${line}`;
})
.join("\n");
process.stdout.write(numberedOutput);
} else {
process.stdout.write(concatenatedContent);
}
} catch (err) {
console.error("Error reading multiple files:", err);
process.exitCode = 1;
}
}

readAndOutputFiles();
6 changes: 3 additions & 3 deletions implement-shell-tools/ls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Your task is to implement your own version of `ls`.

It must act the same as `ls` would, if run from the directory containing this README.md file, for the following command lines:

* `ls -1`
* `ls -1 sample-files`
* `ls -1 -a sample-files`
- `ls -1`
- `ls -1 sample-files`
- `ls -1 -a sample-files`

Matching any additional behaviours or flags are optional stretch goals.

Expand Down
66 changes: 66 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import process from "node:process";
import { promises as fs } from "node:fs";
import { program } from "commander";

program
.option("-1, --one-per-line", "list one file per line")
.option("-a, --all", "do not ignore entries starting with .")
.parse();

const cliOptions = program.opts();
const cliArguments = program.args;

async function runLsCommand() {
try {
// determine directory path (use current directory when none provided)
let directoryPath;
if (cliArguments.length === 0) {
directoryPath = ".";
} else {
directoryPath = cliArguments[0];
}

// read directory entries
const directoryEntries = await fs.readdir(directoryPath);

// filter out dotfiles unless --all was provided
const visibleEntries = [];
if (cliOptions.all) {
for (const name of directoryEntries) {
visibleEntries.push(name);
}
} else {
for (const name of directoryEntries) {
if (!name.startsWith(".")) {
visibleEntries.push(name);
}
}
}

// build output
let outputString = "";
if (cliOptions.onePerLine) {
for (const name of visibleEntries) {
outputString += name + "\n";
}
// if there are no entries, outputString stays empty
} else {
for (let i = 0; i < visibleEntries.length; i++) {
if (i > 0) {
outputString += " ";
}
outputString += visibleEntries[i];
}
if (outputString !== "") {
outputString += "\n";
}
}

process.stdout.write(outputString);
} catch (err) {
console.error("Error reading directory:", err);
process.exitCode = 1;
}
}

runLsCommand();
21 changes: 21 additions & 0 deletions implement-shell-tools/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions implement-shell-tools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "module",
"dependencies": {
"commander": "^14.0.3"
}
}
10 changes: 5 additions & 5 deletions implement-shell-tools/wc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Your task is to implement your own version of `wc`.

It must act the same as `wc` would, if run from the directory containing this README.md file, for the following command lines:

* `wc sample-files/*`
* `wc -l sample-files/3.txt`
* `wc -w sample-files/3.txt`
* `wc -c sample-files/3.txt`
* `wc -l sample-files/*`
- `wc sample-files/*`
- `wc -l sample-files/3.txt`
- `wc -w sample-files/3.txt`
- `wc -c sample-files/3.txt`
- `wc -l sample-files/*`

Matching any additional behaviours or flags are optional stretch goals.

Expand Down
69 changes: 69 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import process from "node:process";
import { promises as fs } from "node:fs";
import { Command } from "commander";

const program = new Command();

program
.name("wc")
.description("A simple node implementation of the word count utility")
.argument("[files...]", "Files to process")
.option("-l, --lines", "print the newline counts")
.option("-w, --words", "print the word counts")
.option("-c, --bytes", "print the byte counts")
.action(async (filePaths, options) => {
const noFlagsProvided = !options.lines && !options.words && !options.bytes;
const shouldShowAllStats = noFlagsProvided;

const allFileStats = [];

for (const filePath of filePaths) {
try {
const fileStats = await calculateFileStats(filePath);
allFileStats.push(fileStats);
printFormattedReport(fileStats, options, shouldShowAllStats);
} catch (error) {
console.error(`wc: ${filePath}: No such file or directory`);
process.exitCode = 1;
}
}

if (allFileStats.length > 1) {
const grandTotals = {
lineCount: allFileStats.reduce((sum, stat) => sum + stat.lineCount, 0),
wordCount: allFileStats.reduce((sum, stat) => sum + stat.wordCount, 0),
byteCount: allFileStats.reduce((sum, stat) => sum + stat.byteCount, 0),
displayName: "total"
};
printFormattedReport(grandTotals, options, shouldShowAllStats);
}
});

async function calculateFileStats(filePath) {
const fileBuffer = await fs.readFile(filePath);
const fileContent = fileBuffer.toString();

const lines = fileContent.split("\n").length - 1;
const words = fileContent.split(/\s+/).filter(word => word.length > 0).length;
const bytes = fileBuffer.length;

return {
lineCount: lines,
wordCount: words,
byteCount: bytes,
displayName: filePath
};
}

function printFormattedReport(stats, options, shouldShowAllStats) {
const outputColumns = [];
const formatColumn = (count) => String(count).padStart(4);

if (shouldShowAllStats || options.lines) outputColumns.push(formatColumn(stats.lineCount));
if (shouldShowAllStats || options.words) outputColumns.push(formatColumn(stats.wordCount));
if (shouldShowAllStats || options.bytes) outputColumns.push(formatColumn(stats.byteCount));

console.log(`${outputColumns.join("")} ${stats.displayName}`);
}

program.parse(process.argv);
Loading