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

program
.name("cat")
.description("concatenate and print files")
.option("-n, --number", "number all output lines")
.option("-b, --number-nonblank", "number nonempty output lines")
.argument("<paths...>", "the file paths to process");

program.parse();

const options = program.opts();
const paths = program.args;

const numberAll = options.number;
const numberNonBlank = options.numberNonblank;

const shouldNumberAll = numberNonBlank ? false : numberAll;

let lineNumber = 1;
let nonBlankNumber = 1;

for (const filePath of paths) {
try {
const content = await fs.readFile(filePath, "utf8");
process.stdout.write(formatContent(content));
} catch (error) {
process.exitCode = 1;
process.stderr.write(`cat: ${filePath}: ${error.message}\n`);
}
}

function formatContent(text) {
const normalized = text.replace(/\r\n/g, "\n");
const endsWithNewline = normalized.endsWith("\n");
const lines = endsWithNewline
? normalized.slice(0, -1).split("\n")
: normalized.split("\n");

const output = [];

for (const line of lines) {
if (numberNonBlank) {
if (line === "") {
output.push("");
} else {
output.push(`${String(nonBlankNumber++).padStart(6, " ")}\t${line}`);
}
} else if (shouldNumberAll) {
output.push(`${String(lineNumber++).padStart(6, " ")}\t${line}`);
} else {
output.push(line);
}
}

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

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

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

program
.name("list files and directories")
.option("-a", "show hidden files")
.option("-1", "force displaying each item in a new line")
.argument("[paths...]", "path of directory");

program.parse();

const showHiddenFiles = program.opts()["a"];
const showFilesInLines = program.opts()["1"];
const paths = program.args.length ? program.args : ["."];

const fetchedDirectories = await fetchDirectoriesFunc(paths);

console.log(formatDisplay(fetchedDirectories));

function formatDisplay(fetchedDirectories) {
const controlDisplaying = fetchedDirectories.map((directoryFilesInArray) => {
const joiner = showFilesInLines ? `\n\r` : ` `;
const showFolderName =
fetchedDirectories.length > 1
? `${directoryFilesInArray.folderName}:\n\r`
: "";

return `${showFolderName}${directoryFilesInArray.files.join(joiner)}`;
});
return controlDisplaying.join("\n\r\n\r");
}

// returns array of objects, like: [{folderName: [file1, file2]}]
async function fetchDirectoriesFunc(directories) {
const result = [];

for (const folderName of directories) {
let files = await fs.readdir(folderName);

// sort by name + but those starting with . at the end
files.sort((a, b) => {
const isHiddenA = a.startsWith(".");
const isHiddenB = b.startsWith(".");

if (isHiddenA !== isHiddenB) {
return isHiddenA ? 1 : -1;
}

const cleanA = a.replace(/^\.+/, "");
const cleanB = b.replace(/^\.+/, "");

return cleanA.localeCompare(cleanB);
});

if (showHiddenFiles) {
files.unshift(".", "..");
} else {
files = files.filter((fileName) => !fileName.startsWith("."));
}

result.push({
folderName,
files,
});
}

return result;
}
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 @@
{
"dependencies": {
"commander": "^14.0.3"
},
"type": "module"
}
82 changes: 82 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { promises as fs } from "node:fs";
import { program } from "commander";

program
.name("print newline, word, and byte counts for each file")
.option("-l", "count lines")
.option("-w", "count words")
.option("-c", "count bytes")
.argument("<paths...>", "file name");

program.parse();

const opts = program.opts();
const flags = Object.keys(opts);
const columns = flags.length > 0 ? flags : ["l", "w", "c"];
const paths = program.args;

async function getFilesData(paths) {
let output = [];

for (const filename of paths) {
try {
const file = await fs.stat(filename);

if (file.isFile()) {
const fileContent = await fs.readFile(filename, "utf-8");
const lineCount = (fileContent.match(/\n/g) || []).length;
const wordCount = fileContent
.trim()
.split(/\s+/)
.filter(Boolean).length;
const fileSize = file.size;

output.push([filename, lineCount, wordCount, fileSize]);
} else {
output.push([filename, `wc: ${filename}: Is a directory`]);
}
} catch (err) {
output.push([filename, `wc: ${filename} ${err.message}`]);
}
}

return output;
}

function displayWcOutput(output, columns) {
const results = output.filter((entry) => entry.length === 4);

const totalLines = results.reduce((sum, e) => sum + e[1], 0);
const totalWords = results.reduce((sum, e) => sum + e[2], 0);
const totalBytes = results.reduce((sum, e) => sum + e[3], 0);

const allRows =
results.length > 1
? [...results, ["total", totalLines, totalWords, totalBytes]]
: results;

const colMap = { l: 1, w: 2, c: 3 };
const w = columns.map((col) =>
Math.max(...allRows.map((e) => String(e[colMap[col]]).length)),
);

const formatRow = (entry) =>
columns
.map((col, i) => String(entry[colMap[col]]).padStart(w[i]))
.join(" ");

for (const entry of output) {
if (entry.length === 2) {
console.log(entry[1]);
} else {
console.log(`${formatRow(entry)} ${entry[0]}`);
}
}

if (results.length > 1) {
const totalsRow = ["total", totalLines, totalWords, totalBytes];
console.log(`${formatRow(totalsRow)} total`);
}
}

displayWcOutput(await getFilesData(paths), columns);
Loading