Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
04e0f69
cat initial implementation
RaihanSharif Jul 21, 2026
8cadca9
a file to test cat functionality
RaihanSharif Jul 21, 2026
8294ca8
redo: parsing arguments
RaihanSharif Jul 23, 2026
7144124
Error message if no file supplied.
RaihanSharif Jul 23, 2026
aeca1f7
rename file to path
RaihanSharif Jul 23, 2026
1d9d1df
finished cat
RaihanSharif Jul 23, 2026
d577899
capture flag arguments
RaihanSharif Jul 23, 2026
4d0e873
working subdirectories
RaihanSharif Jul 23, 2026
75e9d0a
initial working version
RaihanSharif Jul 23, 2026
b77ab50
redo: function to get entries for a given path
RaihanSharif Jul 23, 2026
abce30a
printing entries for a single path
RaihanSharif Jul 23, 2026
98c1c43
ls done
RaihanSharif Jul 23, 2026
5e36e1f
fix: cat line numbering is per file, not on the concatanation of files
RaihanSharif Jul 25, 2026
1a27139
mode switching arg parser
RaihanSharif Jul 25, 2026
667e3bb
clean up
RaihanSharif Jul 27, 2026
c7449be
Parse optoins using commander
RaihanSharif Jul 27, 2026
45766e0
parse file arguments
RaihanSharif Jul 27, 2026
2c3458f
handle no flag arguments
RaihanSharif Jul 27, 2026
3c8e345
shows error message if trying to read a dictory
RaihanSharif Jul 27, 2026
1a80d5b
prints lwc for one or more files
RaihanSharif Jul 27, 2026
bdfbbba
print totals in bottom row if more than one file
RaihanSharif Jul 27, 2026
0b3d37d
correctly print out totals, includning flags
RaihanSharif Jul 27, 2026
0741a9a
add 5 leading spaces to cat numbered output
RaihanSharif Aug 2, 2026
add67c9
add the dots in ls
RaihanSharif Aug 2, 2026
d7ddea2
sort entries before printing
RaihanSharif Aug 2, 2026
c8e7c29
remove chownSync from import
RaihanSharif Aug 2, 2026
bc623a1
wc accounts for invalid file/folder paths
RaihanSharif Aug 2, 2026
9135017
Ad propper padding to lines
RaihanSharif Aug 3, 2026
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
65 changes: 65 additions & 0 deletions implement-shell-tools/cat/cat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { readFileSync } from "node:fs";
import process from "node:process";

// Note: takes only one flag, -n or -b, and accepts whatever the last flag is
// can parse flag from any position in args

// capturing the user args
const args = process.argv.slice(2);

let flag;
const paths = [];

// the * (glob expansion is done automatically by zsh, bash etc. on linux)
for (const arg of args) {
if (arg === "-n" || arg === "-b") {
flag = arg;
} else {
paths.push(arg);
}
}

// if no file is supplied exit with error
if (paths.length === 0) {
console.error("usage: cat [-n] <file...>");
process.exit(1);
}

// starting file number, if lines need to be prepended

for (const path of paths) {
let lineNum = 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You declare let lineNum = 1 inside the for (const path of paths) loop. When you run cat -n sample-files/*.txt against the real cat, does the numbering restart at 1 for each file, or keep climbing across all of them? Where would lineNum need to be declared to match what you see?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

cat -n sample-files/*.txt
     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...
node cat.js -n sample-files/*.txt
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...

I don't see the difference. Other than the spacing at the start of each line.
I initally had the lineNum outside the loop so that the numbers would increment across files.
You can see this in the commit 5e36e1fccc18b08a5495f9ede132c9ed0b0cfa4b

let file;
try {
// using sync as it's a simple short program
file = readFileSync(path, "utf-8");
} catch (err) {
console.error(`cat: ${path}: ${err.message}`);
continue; // Real cat continues to next file if current file not found
}

const lines = file.split("\n");

// remove trailing empty line as this how real cat works
if (lines[lines.length - 1] === "") lines.pop();

if (flag === "-n") {
for (const line of lines) {
console.log(`${String(lineNum).padStart(6, " ")}\t${line}`);
lineNum++;
}
} else if (flag === "-b") {
for (const line of lines) {
if (line === "") {
console.log(line);
} else {
console.log(`${String(lineNum).padStart(6, " ")}\t${line}`);
lineNum++;
}
}
} else {
for (const line of lines) {
console.log(line);
}
}
}
13 changes: 13 additions & 0 deletions implement-shell-tools/cat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "cat",
"version": "1.0.0",
"description": "You should already be familiar with the `cat` command line tool.",
"main": "cat.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module"
}
71 changes: 71 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import fs from "node:fs";
import process from "node:process";

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

const flags = new Set();
let paths = [];

let isFlag = true;
for (const arg of args) {
if (isFlag && arg === "--") {
isFlag = false;
} else if (isFlag && arg.startsWith("-") && arg !== "-") {
// capture the flags without the -
// supports combined flags like -1a
for (const ch of arg.slice(1)) {
flags.add(ch);
}
} else {
paths.push(arg);
}
}

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

// returns all entries for a given path
// if -a flag, then include dotfiles, else exclude dotfiles
function getPathEntries(path, aFlag = flags.has("a")) {
let entries = fs.readdirSync(path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Compare node ls.js -1 -a sample-files to the real ls -1 -a sample-files, line by line. Which two entries does the real one show that yours doesn't? Does the fs.readdirSync call on line 31 ever return those, and if not, where could they come from?

@RaihanSharif RaihanSharif Aug 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You mean this?

. 
. .

Those are current and parent directories. I figured it'd be ok to leave them out as they're not captured by process.

I've added them now.

function getPathEntries(path, aFlag = flags.has("a")) {
    let entries = fs.readdirSync(path);
    entries = [".", "..", ...entries];

    if (!aFlag) {
        entries = entries.filter((e) => !e.startsWith("."));
    }
    return entries;
}

Appears to be working as expected.

entries = [".", "..", ...entries];
entries.sort();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your output matches here, but is the order fs.readdirSync (line 31) returns entries in actually guaranteed? What does real ls do to its list before printing — and would yours still match on a machine that read the directory in a different order?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No it's not guaranteed. I made an assumption given the entries appears to be in the right order each time.

Real ls orders the path strings alphabetically before output.

I have now added a line to sort the entries.

function getPathEntries(path, aFlag = flags.has("a")) {
    let entries = fs.readdirSync(path);
    entries = [".", "..", ...entries];
    entries.sort();  // sort before output.
    if (!aFlag) {
        entries = entries.filter((e) => !e.startsWith("."));
    }
    return entries;
}

if (!aFlag) {
entries = entries.filter((e) => !e.startsWith("."));
}
return entries;
}

// formatter: if -1 flag, print entry per line
// else all in one line with
function printEntries(entries, onePerLineFlag = flags.has("1")) {
if (onePerLineFlag) {
entries.forEach((e) => console.log(e));
} else {
// if join on empty entries arr, add extra blank line
if (entries.length !== 0) {
console.log(entries.join("\t"));
}
}
}

// this is needed to group files at the top and folers at the bottom when giving mutiple path arguements
// to argv e.g. node ls.js sample-files/*
const fileArgs = paths.filter((p) => !fs.statSync(p).isDirectory());
const dirArgs = paths.filter((p) => fs.statSync(p).isDirectory());

// First print all plain file arguments together, as one group
if (fileArgs.length > 0) {
printEntries(fileArgs);
}

// Then print each directory's listing, with headers if needed
dirArgs.forEach((path, index) => {
if (paths.length > 1) {
if (index > 0 || fileArgs.length > 0) console.log("");
console.log(`${path}:`);
}
printEntries(getPathEntries(path));
});
25 changes: 25 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.

16 changes: 16 additions & 0 deletions implement-shell-tools/wc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "wc",
"version": "1.0.0",
"description": "You should already be familiar with the `wc` command line tool.",
"main": "wc.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"commander": "^15.0.0"
}
}
88 changes: 88 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { program } from "commander";
import fs from "node:fs";
import process from "node:process";

program
.name("wc")
.description("Mimics the wc command line tool")
.option("-w")
.option("-c")
.option("-l")
.argument("<files...>", "files to process");

program.parse();

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

// if no -lwc flags are supplied, wc prints
// lines, words, bytes of each file
// whereas if any flags are supplied only those
// values are printed
if (Object.keys(options).length === 0) {
options.l = options.w = options.c = true;
}

// keeps track of total values for each of the data
// incrementally updated in the loop below
const totals = { l: 0, w: 0, c: 0 };

// wc actually prints total if there is more than one argument, not more than one file
// e.g. wc invalid.txt valid.txt
// there will be a total line with the same stats are the valid.txt stats
let fileCount = 0;

for (const path of paths) {
let isDir;
try {
isDir = fs.statSync(path).isDirectory();
} catch (err) {
console.error(`wc: ${path} open: No such file or directory`);
fileCount++;
continue;
}

if (isDir) {
console.eror(`wc: ${path}: read: Is a directory`);
} else {
fileCount++;
let outputStr = "";
const file = fs.readFileSync(path, "utf-8");
if (options.l) {
const lines = file.split("\n");
// exclude trailing empty line from count
if (lines.at(-1) === "") {
lines.pop();
}
const lineCount = lines.length;
totals.l += lineCount;
outputStr += `\t${lineCount}`;
}

if (options.w) {
// real wc splits not just on " ", but on white spaces more generally
const words = file.split(/\s+/).filter(Boolean);
const wordCount = words.length;
totals.w += wordCount;
outputStr += `\t${wordCount}`;
}

if (options.c) {
file.size;
const byteCount = fs.statSync(path).size;
totals.c += byteCount;
outputStr += `\t${byteCount}`;
}

outputStr += ` ${path}`;
console.log(outputStr);
}
}

// if there's more than one file, print out a total
// if a flag is not selected, then the value for that flag is 0
// filter out anything with a total of 0
if (fileCount > 1) {
const totalsArr = Object.values(totals).filter((elem) => elem !== 0);
console.log(`\t${totalsArr.join("\t")} total`);
}
Loading