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
38 changes: 38 additions & 0 deletions implement-shell-tools/cat/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { program } from "commander";
import * as fs from "node:fs/promises";

program
.name("cat")
.description("Reads file(s) and writes them to the standard output")
.argument("<paths...>", "The file path(s) to process")
.option("-n", "Number the output lines, starting at 1.")
.option("-b", "Number only non-blank output lines, starting at 1.");

program.parse();

try {
const filePaths = program.args;

const options = program.opts();

for (const filePath of filePaths) {
const file = await fs.open(filePath);

let lineNum = 1;

try {
for await (const line of file.readLines()) {
const isBlank = line.trim() === "";
const shouldNumber = options.n || (options.b && !isBlank);

console.log(shouldNumber ? `${lineNum} ${line}` : line);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match the cat output accurately you would need to add the correct padding instead of just spaces. Here it's meant to be padding of 6 so the output is right-aligned.


if (shouldNumber) lineNum++;
}
} finally {
await file.close();
Comment on lines +32 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of finally to close the file after reading

}
}
} catch (err) {
console.error(err.message);
}
21 changes: 21 additions & 0 deletions implement-shell-tools/cat/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/cat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "module",
"dependencies": {
"commander": "^14.0.3"
}
}
67 changes: 67 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { program } from "commander";
import * as fs from "node:fs/promises";

program
.name("ls")
.description("List directory contents")
.argument(
"[paths...]",
"The file path to process (defaults to current directory)",
)
.option("-a", "Include directory entries whose names begin with a dot ('.').")
.option("-1", "Force output to be one entry per line.");

program.parse();

try {
let filePaths = program.args;
if (!filePaths || filePaths.length === 0) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue !filePaths is a bad path you weren't asked to account for

filePaths = ["."];
}

const options = program.opts();
const includeHidden = Boolean(options.a);
const onePerLine = Boolean(options["1"]);

const result = { files: [], dirs: {} };

for (const filePath of filePaths) {
const stats = await fs.stat(filePath);
if (stats.isFile()) result.files.push(filePath);
if (stats.isDirectory()) {
result.dirs[filePath] = await fs.readdir(filePath);
}
}

const filterHidden = (files) => files.filter((file) => !file.startsWith("."));

const getVisibleEntries = (files) =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally dislike having a function like getVisibleEntries be dependent on a variable outside of this function. I'd prefer you to pass in includeHidden

It breaks the idea of "purity" (a function's output depends solely on arguments passed in or internal logic, no external dependencies - which includeHidden is a hidden dependency). It makes it hard to test (we need dependencies more loosely coupled), and harder to reuse

includeHidden ? files : filterHidden(files);

const formatEntries = (files) => {
if (files.length === 0) return;
console.log(files.join(onePerLine ? "\n" : "\t"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the examples you've been asked to accommodate, there is always a newline option (-1) so the onePerLine check could be seen as unnecessary. (I'd argue you could've assumed onePerLine across this solution but I am treating this as a stretch behaviour to have it optional)

I agree the use of \t is an easier fix to calculating the column padding needed, but maybe less accurate to the real ls output columns.

};

result.files = getVisibleEntries(result.files);

if (filePaths.length === 1) {
let entries = [...result.files];

for (const [dir, contents] of Object.entries(result.dirs)) {
const filtered = getVisibleEntries(contents);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(based on previous comment) we can see here how getVisibleEntries leaves a lot of questions - how is it determining what is visible or not? Is this a hard coded process, or is it a changeable output?

entries = entries.concat(filtered);
}
formatEntries(entries);
} else {
formatEntries(result.files);

for (const [dir, contents] of Object.entries(result.dirs)) {
console.log("\n" + dir + ":");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small bug here - you're baking into your output a newline on the first line of prints - not an expected behaviour, the first line should be at the top with no newline

How can you separate out the concern so the newline gaps are between directory sections only and the top section doesn't have a newline before it?

const filtered = getVisibleEntries(contents);
formatEntries(filtered);
}
}
} catch (err) {
console.error(err.message);
}
21 changes: 21 additions & 0 deletions implement-shell-tools/ls/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/ls/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "module",
"dependencies": {
"commander": "^14.0.3"
}
}
21 changes: 21 additions & 0 deletions implement-shell-tools/wc/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/wc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "module",
"dependencies": {
"commander": "^14.0.3"
}
}
65 changes: 65 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { program } from "commander";
import * as fs from "node:fs/promises";

program
.name("wc")
.description("word, line and byte count")
.argument("<paths...>", "The file path(s) to process.")
.option(
"-l, --lines",
"The number of lines in each input file is written to the standard output.",
)
.option(
"-w, --words",
"The number of words in each input file is written to the standard output.",
)
.option(
"-c --bytes",
"The number of bytes in each input file is written to the standard output.",
);

program.parse();

try {
const filePaths = program.args;
const results = {};

for (const filePath of filePaths) {
const file = await fs.open(filePath);
const stats = await fs.stat(filePath);
const count = { lines: 0, words: 0, bytes: stats.size };

try {
for await (const line of file.readLines()) {
count.lines++;
const trimmed = line.trim();
if (trimmed.length > 0) {
count.words += trimmed.split(/\s+/).length;
}
}
} finally {
await file.close();
}
results[filePath] = count;
}

if (filePaths.length > 1) {
const total = { lines: 0, words: 0, bytes: 0 };
for (const file of Object.values(results)) {
total.lines += file.lines;
total.words += file.words;
total.bytes += file.bytes;
}
results["total"] = total;
}

const options = program.opts();
const noOptionsProvided = !Object.keys(options).length;
const selectedOptionKeys = [...Object.keys(options)];

noOptionsProvided
? console.table(results)
: console.table(results, selectedOptionKeys);
} catch (err) {
console.error(err.message);
}
Loading