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
53 changes: 53 additions & 0 deletions implement-shell-tools/cat/cat.mjs
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 { Command } from "commander";

const program = new Command();
program
.name("cat")
.description("Concatenate files and print them to stdout")
.option("-n, --number", "number all output lines")
.option("-b, --number-nonblank", "number non-blank lines")
.argument("<paths...>", "Paths to process");

program.parse();

const options = program.opts();
const paths = program.args;
let lineNumber = 1;

for (const path of paths) {
const contents = await fs.readFile(path, "utf8");
const endsWithNewline = contents.endsWith("\n");

let lines = contents.split("\n");

if (endsWithNewline) {
lines.pop();
}

if (options.numberNonblank) {
lines = numberLines(lines, false);
} else if (options.number) {
lines = numberLines(lines, true);
}

let output = lines.join("\n");

if (endsWithNewline) {
output += "\n";
}

process.stdout.write(output);
}

function numberLines(lines, includeBlankLines) {
for (let i = 0; i < lines.length; i++) {
if (includeBlankLines || lines[i].length > 0) {
lines[i] = `${String(lineNumber).padStart(6, " ")}\t${lines[i]}`;
lineNumber++;
}
}

return lines;
}
126 changes: 126 additions & 0 deletions implement-shell-tools/ls/ls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import process from "node:process";
import fs, { truncate } from "node:fs";

const argv = process.argv.slice(2);

const options = {
onePerLine: false,
all: false,
help: false,
};

const validFlags = {
onePerLine: {
short: "-1",
long: "--one-per-line",
description: "Lists one file per line",
},
all: {
short: "-a",
long: "--all",
description: "Include entries whose names begin with a dot",
},
help: {
short: "-h",
long: "--help",
description: "Display help information",
},
};

const paths = [];
let parsingFlags = true;

for (const arg of argv) {
if (arg === "--") {
parsingFlags = false;
} else if (parsingFlags && arg.startsWith("--")) {
const matchedFlag = Object.entries(validFlags).find(
([, definition]) => arg === definition.long,
);

if (matchedFlag) {
const [optionName] = matchedFlag;
options[optionName] = true;
} else {
process.stderr.write(`Invalid option -- '${arg}'\n`);
process.exit(1);
}
} else if (parsingFlags && arg.startsWith("-")) {
const shortFlags = arg.slice(1);

for (const shortFlag of shortFlags) {
const matchedFlag = Object.entries(validFlags).find(
([, definition]) => definition.short === `-${shortFlag}`,
);

if (matchedFlag) {
const [optionName] = matchedFlag;
options[optionName] = true;
} else {
process.stderr.write(`Invalid option -- '${arg}\n'`);
process.exit(1);
}
}
} else {
paths.push(arg);
}
}

if (options.help) {
process.stdout.write("Usage: ls [OPTION]... [FILE]...\n");
process.stdout.write(
"List information about the FILEs (the current directory by default).\n\n",
);
process.stdout.write("Options:\n");

for (const definition of Object.values(validFlags)) {
process.stdout.write(
` ${definition.short}, ${definition.long}\t${definition.description}\n`,
);
}

process.exit(0);
}

if (paths.length === 0) {
paths.push(".");
}

const multiplePaths = paths.length > 1;
const filePaths = [];
const directoryPaths = [];

for (const path of paths) {
const stats = fs.statSync(path);

if (stats.isDirectory()) {
directoryPaths.push(path);
} else {
filePaths.push(path);
}
}

if (filePaths.length > 0) {
const separator = options.onePerLine ? "\n" : " ";
process.stdout.write(`${filePaths.join(separator)}\n`);
}

for (const path of directoryPaths) {
if (multiplePaths) {
process.stdout.write(`\n${path}: \n`);
}
let content = fs.readdirSync(path);

if (options.all) {
content.unshift(".", "..");
} else {
content = content.filter((entry) => !entry.startsWith("."));
}

content.sort();

const separator = options.onePerLine ? "\n" : " ";
const output = content.join(separator);

process.stdout.write(`${output}\n`);
}
20 changes: 20 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.

5 changes: 5 additions & 0 deletions implement-shell-tools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"commander": "^15.0.0"
}
}
118 changes: 118 additions & 0 deletions implement-shell-tools/wc/wc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import process from "node:process";
import fs from "node:fs";
import { parseArgs } from "node:util";

const options = {
lines: {
type: "boolean",
short: "l",
},
words: {
type: "boolean",
short: "w",
},
bytes: {
type: "boolean",
short: "c",
},
};

const { values, positionals } = parseArgs({
options,
allowPositionals: true,
});

if (positionals.length === 0) {
process.stderr.write("Please provide a file.\n");
process.exit(1);
}

const fileCounts = [];

for (const path of positionals) {
fileCounts.push(countFile(path));
}

const totalCounts = {
lineCount: 0,
wordCount: 0,
byteCount: 0,
};

for (const file of fileCounts) {
totalCounts.lineCount += file.lineCount;
totalCounts.wordCount += file.wordCount;
totalCounts.byteCount += file.byteCount;
}

// output formatting
const largestByteCount =
fileCounts.length > 1 ? totalCounts.byteCount : fileCounts[0].byteCount;

const width = String(largestByteCount).length;

for (const file of fileCounts) {
const formattedCounts = formatCounts(file, width);

process.stdout.write(`${formattedCounts} ${file.path}\n`);
}

if (fileCounts.length > 1) {
const formattedTotals = formatCounts(totalCounts, width);

process.stdout.write(`${formattedTotals} total\n`);
}

function countFile(path) {
const content = fs.readFileSync(path, "utf-8");

const lineCount = [...content].filter((char) => char === "\n").length;

const trimmedContent = content.trim();

const wordCount =
trimmedContent === "" ? 0 : trimmedContent.split(/\s+/).length;

const byteCount = Buffer.byteLength(content);

return {
lineCount,
wordCount,
byteCount,
path,
};
}

function getSelectedCounts(lineCount, wordCount, byteCount) {
const selectedCounts = [];

if (values.lines) {
selectedCounts.push(lineCount);
}

if (values.words) {
selectedCounts.push(wordCount);
}

if (values.bytes) {
selectedCounts.push(byteCount);
}

if (selectedCounts.length === 0) {
selectedCounts.push(lineCount, wordCount, byteCount);
}

return selectedCounts;
}

function formatCounts(counts, width) {
const selectedCounts = getSelectedCounts(
counts.lineCount,
counts.wordCount,
counts.byteCount,
);

return selectedCounts
.map((count) => String(count).padStart(width))
.join(" ");
}
Loading