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

program
.name("cat")
.description("Concatenate and print files")
.option("-n", "Number the output lines, starting at 1")
.option("-b", "Number the non-blank output lines, starting at 1")
.argument("<sample-files...>", "The file path to process")
.parse();

const argv = program.args;

const opts = program.opts();

const countLines = (data) => {
const lines = data.split("\n");
if (lines[lines.length - 1] === "") {
lines.pop();
}

let lineNum = 1;

Choose a reason for hiding this comment

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

If I pass multiple files in, the line numbering resets to 1 for each file. Can you make your implementation work more like the original cat which uses a continuous numbering?

Copy link
Author

Choose a reason for hiding this comment

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

Since I’m on macOS, the built-in cat also resets line numbers to 1 for each file. Its output looks like this:

cyf@cyfs-MacBook-Pro cat % cat -n sample-files/*
     1  Once upon a time...
     1  There was a house made of gingerbread.
     1  It looked delicious.
     2  I was tempted to take a bite of it.
     3  But this seemed like a bad idea...
     4
     5  There's more to come, though...


for (const line of lines) {
if (opts.b) {
if (line.trim() === "") {
console.log();
} else {
console.log(`${lineNum} ${line}`);
lineNum++;
}
} else if (opts.n) {
console.log(`${lineNum} ${line}`);
lineNum++;
}
}
};

async function example(path) {
try {
const data = await fs.readFile(path, { encoding: "utf8" });
if (opts["b"]) {
countLines(data);
} else if (opts["n"]) {
countLines(data);
} else {
console.log(data.trimEnd());
}
} catch (err) {
console.error(err);
}
}

const handleInput = async () => {
for (const path of argv) {
await example(path);
}
};

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

program
.name("ls")
.description("Shows files in directory")
.option("-1", "list one file per line")
.option(
"-a",
"Used to list all files, including hidden files, in the current directory"
)
.argument("[sample-files]", "The file path to process");

program.parse();

let pathToFile = "";

const programArgv = program.args;

(async () => {
if (programArgv.length === 1) {
pathToFile = programArgv[0];
try {
const stats = await fs.stat(pathToFile);
if (stats.isFile()) {
await listFiles("file");
} else if (stats.isDirectory()) {
listFiles("directory");
} else {
console.error("Not a file or directory.");
}
} catch (err) {
console.error("Invalid path:", err.message);
}
} else if (programArgv.length === 0) {
pathToFile = process.cwd();
await listFiles("directory");
} else {
console.error(
`Expected no more than 1 argument (sample-files) but got ${programArgv.length}.`
);
}
})();

const flag_1 = (files) => {
files.forEach(function (file) {
console.log(file);
});
};

const flag_a = (files) => {
files.unshift("..");
files.unshift(".");
return files;
};

async function listFiles(type) {
let output = [];
let formattedPath = "";
if (type == "directory") {
formattedPath = pathToFile;
} else if (type == "file") {
formattedPath = path.dirname(pathToFile);
}
const char = program.opts();
const files = await fs.readdir(formattedPath);
const sortedOutput = files.sort((a, b) => a.localeCompare(b));

if (char["a"]) {
output = flag_a(sortedOutput);
} else {
sortedOutput.forEach(function (file) {
if (file[0] != ".") {
output.push(file);
}
});
}

if (char["1"]) {
flag_1(output);
} else {
console.log(output.join(" "));
}
}
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.0"
}
}
95 changes: 95 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { program } from "commander";
import { promises as fs } from "node:fs";

program
.name("count-containing-words")
.description("Counts words in a file that contain a particular character")
.option(
"-l",
"The number of lines in each input file is written to the standard output."
)
.option(
"-w",
"The number of words in each input file is written to the standard output."
)
.option(
"-c",
"The number of bytes in each input file is written to the standard output."
)
.argument("<path...>", "The file path to process")
.parse();

const argv = program.args;

const opts = program.opts();

const total = [];
const output = [];
const countsPerFile = [];
let columnWidth = 0;

const flag_c = (content) => {
return Buffer.byteLength(content, "utf8");
};

const flag_w = (content) => {
return content.match(/\b[\w']+\b/g).length;
};

const flag_l = (content) => {
return content.split("\n").length - 1;
};

const countAndDisplay = async (path) => {
const outputPerFile = [];
const content = await fs.readFile(path, "utf-8");
if (opts["l"]) {
outputPerFile.push(flag_l(content));
}
if (opts["w"]) {
outputPerFile.push(flag_w(content));
}
if (opts["c"]) {
outputPerFile.push(flag_c(content));
}
if (argv.length > 1) {
if (total.length == 0) {
total.push(...outputPerFile);
} else {
for (let index = 0; index < outputPerFile.length; index++) {
total[index] += outputPerFile[index];
}
}
countsPerFile.push(...outputPerFile);
}
outputPerFile.push(path);
output.push([...outputPerFile]);
};

const handleInput = async () => {
if (Object.keys(opts).length == 0) {
["l", "w", "c"].forEach((key) => (opts[key] = true));
}
for (const path of argv) {
await countAndDisplay(path);
}
const numOfColumns = Object.keys(opts).length;
if (argv.length > 1) {
total.push("total");
output.push(total);
}
for (const row of output) {
for (let i = 0; i < numOfColumns; i++) {
columnWidth = Math.max(columnWidth, String(row[i]).length);
}
}

for (let row of output) {
const line_parts = [];
for (let i = 0; i < numOfColumns; i++) {
line_parts.push(String(row[i]).padStart(columnWidth + 4));
}
console.log(line_parts.join(" "), row.at(-1));
}
};
handleInput();