Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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