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
88 changes: 88 additions & 0 deletions implement-shell-tools/cat/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env node
// cat.js — ESM: supports -n and -b (-b overrides -n)

import fs from "node:fs";
import readline from "node:readline";

const args = process.argv.slice(2);
let numberAll = false;
let numberNonblank = false;
const files = [];

// parse flags + files (flags can appear anywhere)
for (const a of args) {
if (a === "-n") numberAll = true;
else if (a === "-b") numberNonblank = true;
else files.push(a);
}
// precedence: -b wins over -n
if (numberNonblank) numberAll = false;

if (files.length === 0) {
console.error("Usage: node cat.js [-n|-b] <file...>");
process.exit(1);
}

let hadError = false;
let lineNo = 1;

for (const file of files) {
try {
const stat = await fs.promises.stat(file);
if (stat.isDirectory()) {
console.error(`cat: ${file}: Is a directory`);
hadError = true;
continue;
}
if (!numberAll && !numberNonblank) {
await pipeFile(file);
} else {
await numberFile(file, { nonblank: numberNonblank });
}
} catch (err) {
if (err?.code === "ENOENT" || err?.code === "ENOTDIR") {
console.error(`cat: ${file}: No such file or directory`);
} else {
console.error(`cat: ${file}: ${err?.message || "Error"}`);
}
hadError = true;
}
}

if (hadError) process.exitCode = 1;

function pipeFile(file) {
return new Promise((resolve, reject) => {
const rs = fs.createReadStream(file);
rs.on("error", reject);
rs.on("end", resolve);
rs.pipe(process.stdout, { end: false }); // keep stdout open between files
});
}

function numberFile(file, { nonblank }) {

Choose a reason for hiding this comment

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

Is there a reason why you would want two separate functions for printing out content normally and numbered? Could these be combined into one function?

return new Promise((resolve, reject) => {
const rs = fs.createReadStream(file);
const rl = readline.createInterface({ input: rs, crlfDelay: Infinity });

rl.on("line", (line) => {
if (nonblank) {
if (line.length === 0) {
process.stdout.write("\n"); // blank line, no number
} else {
process.stdout.write(formatNum(lineNo++) + line + "\n");
}
} else {
process.stdout.write(formatNum(lineNo++) + line + "\n");
}
});
rl.on("close", resolve);
rl.on("error", reject);
rs.on("error", reject);
});
}

function formatNum(n) {
// GNU cat style: width 6, right-aligned, then a tab
return String(n).padStart(6, " ") + "\t";
}
82 changes: 82 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env node


import fs from "node:fs";
import { pathToFileURL } from "node:url";

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

let includeAll = false; // -a
let onePerLine = false; // -1 (format already one-per-line)

Choose a reason for hiding this comment

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

As it defaults to one per line, this argument is not doing anything. What change would you need to make to the program to ensure that the default behaviour is to output all on the same line?

const targets = [];

// Parse flags (supports combined like -a1 / -1a). Treat lone "-" as a path.
for (const arg of raw) {

Choose a reason for hiding this comment

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

Is there an advantage of writing argument parsing yourself rather than using an API or library?

if (arg === "-" || !arg.startsWith("-")) {
targets.push(arg);
continue;
}
if (arg === "-1") {
onePerLine = true;
continue;
}
for (const ch of arg.slice(1)) {
if (ch === "a") includeAll = true;
else if (ch === "1") onePerLine = true;
else {
// ignore unknown short flags
}
}
}

const paths = targets.length ? targets : ["."];
let hadError = false;

for (let i = 0; i < paths.length; i++) {
const p = paths[i];
try {
const st = await fs.promises.lstat(p);

if (st.isDirectory()) {
if (paths.length > 1) console.log(`${p}:`);

const entries = await fs.promises.readdir(p, { withFileTypes: true });
let names = entries.map(d => d.name);

if (!includeAll) {
names = names.filter(n => !n.startsWith("."));
} else {
// mimic `ls -a` by including "." and ".."
names = ["." , "..", ...names];
}

names.sort((a, b) => a.localeCompare(b));
for (const name of names) {
// one-per-line output; -1 flag just affirms it
console.log(name);
}

if (paths.length > 1 && i !== paths.length - 1) console.log("");
} else {
// file or symlink => print the argument as given
console.log(p);
}
} catch (err) {
if (err?.code === "ENOENT") {
console.error(`ls: cannot access '${p}': No such file or directory`);
} else if (err?.code === "EACCES") {
console.error(`ls: cannot open directory '${p}': Permission denied`);
} else {
console.error(`ls: ${p}: ${err?.message || "Error"}`);
}
hadError = true;
}
}

if (hadError) process.exitCode = 1;

// run only when executed directly
const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href;
if (!isDirect) {
// allow importing in tests without auto-executing
}
12 changes: 12 additions & 0 deletions implement-shell-tools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "module",
"name": "implement-shell-tools-with-node",
"version": "1.0.0",
"description": "Your task is to re-implement shell tools you have used.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "pezhman azizi",
"license": "ISC"
}
115 changes: 115 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env node
// wc.js — ESM: supports flags -l (lines), -w (words), -c (bytes)
// Behaviors covered by the README:
// - 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/*
// Also works with multiple flags (e.g., -lw), like GNU wc.

import fs from "node:fs";
import { pathToFileURL } from "node:url";

async function main() {
const argv = process.argv.slice(2);

let showLines = false;
let showWords = false;
let showBytes = false;
const files = [];

// Parse flags + files. Support combined short flags like -lw, -cl, etc.
for (const arg of argv) {
if (arg.startsWith("-") && arg !== "-") {
for (const ch of arg.slice(1)) {
if (ch === "l") showLines = true;
else if (ch === "w") showWords = true;
else if (ch === "c") showBytes = true;
else {
// ignore unknown short options for this assignment
}
}
} else {
files.push(arg);
}
}

if (files.length === 0) {
console.error("Usage: node wc.js [-l|-w|-c] <file...>");
process.exit(1);
}

// No flags → show all three like wc
const showAll = !showLines && !showWords && !showBytes;

let hadError = false;
let total = { lines: 0, words: 0, bytes: 0 };
const results = [];

for (const file of files) {
try {
const st = await fs.promises.lstat(file);
if (st.isDirectory()) {
console.error(`wc: ${file}: Is a directory`);
hadError = true;
continue;
}
const counts = await countFile(file);
results.push({ file, ...counts });
total.lines += counts.lines;
total.words += counts.words;
total.bytes += counts.bytes;
} catch (err) {
if (err?.code === "ENOENT") {
console.error(`wc: ${file}: No such file or directory`);
} else if (err?.code === "EACCES") {
console.error(`wc: ${file}: Permission denied`);
} else {
console.error(`wc: ${file}: ${err?.message || "Error"}`);
}
hadError = true;
}
}

for (const r of results) {
console.log(formatRow(r, { showAll, showLines, showWords, showBytes }));
}
if (results.length > 1) {
console.log(formatRow({ file: "total", ...total }, { showAll, showLines, showWords, showBytes }));
}

if (hadError) process.exitCode = 1;
}

async function countFile(file) {
const buf = await fs.promises.readFile(file); // Buffer
const bytes = buf.length;

// Lines: count '\n' bytes
let lines = 0;
for (let i = 0; i < buf.length; i++) if (buf[i] === 0x0a) lines++;

// Words: sequences of non-whitespace
const text = buf.toString("utf8");
const words = (text.match(/\S+/g) || []).length;

return { lines, words, bytes };
}

function formatRow({ lines, words, bytes, file }, opts) {
const cols = [];
if (opts.showAll || opts.showLines) cols.push(pad(lines));
if (opts.showAll || opts.showWords) cols.push(pad(words));
if (opts.showAll || opts.showBytes) cols.push(pad(bytes));
return `${cols.join(" ")} ${file}`;
}

function pad(n) {
// Right-align like `wc`
return String(n).padStart(7, " ");
}

// run only when executed directly
const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href;
if (isDirect) await main();
Loading