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
4 changes: 4 additions & 0 deletions implement-shell-tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
*.log
.DS_Store
.env
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 { program } from "commander";
import { promises as fs } from "node:fs";
import process from "node:process";

// configure the CLI program with its name, description, arguments, options, and actions (the help instructions)
program
.name("cat")
.description("An alternative to the 'cat' command")
.argument("<files...>", "The file(s) to process")
.option("-n, --number", "Number all output lines")
.option("-b, --number-nonblank", "Number non-blank output lines")
.action(async (files, options) => {
try {
await newCat(files, options);
} catch (err) {
console.error(`Error: ${err.message}`);
}
});

program.parse(process.argv);

//helper function to format output
function printLine(line, lineNumber, padWidth) {
if (lineNumber !== null) {
console.log(`${lineNumber.toString().padStart(padWidth, ' ')} ${line}`);
} else {
console.log(line);
}
}

async function newCat(files, options) {
let lineNumber = 1;
const padWidth = 6;

for (const file of files) {
// read each file into a single text string
try {
const data = await fs.readFile(file, "utf8");
// split that string into an array at \n where each element is a line from the file
// e.g. lines = ["Line 1", "Line 2", "Line 3"]
const lines = data.split("\n")

// remove trailing blank line caused by a trailing newline
if (lines[lines.length - 1] === "") {
lines.pop();
}

lines.forEach(line => {
//line trim: truthy = text, falsy = blank
if (options.numberNonblank && line.trim()) {
// number non-blank lines only
printLine(line, lineNumber++, padWidth);
} else if (options.number){
// number all lines
printLine(line, lineNumber++, padWidth);
} else {
// neither flag, print normally
printLine(line, null, padWidth)
}
});
} catch (err) {
console.error(`Error reading file ${file}: ${err.message}`);
}
}
}
94 changes: 94 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { program } from "commander";
import { promises as fs } from "node:fs";
import process from "node:process";

// configure the CLI program with its name, description, arguments, options, and actions (the help instructions)
program
.name("ls")
.description("An alternative to the 'ls' command")
.argument("[directory]", "The directory to list")
// Commander stores -1 as a string key that is accessed using options['1']
.option("-1", "List all files, one per line")
.option("-a, --all", "Include hidden files (those starting with .) in the listing")
.action(async (directory, options) => {
try {
// default to current directory if none is specified
const dir = directory || ".";

await newLs(dir, options['1'], options.all);
} catch (err) {
console.error(`Error: ${err.message}`);
}
});

program.parse(process.argv);


// filter files based on visibility (includeHidden = true includes all files)
function filterFiles(entries, includeHidden) {
return entries.filter(name =>
includeHidden ? true : !name.startsWith(".")
);
}

// sort entries: directories first, then files,
function sortEntries(entries) {
const dirs = entries.filter(entry => {
try {
return fs.statSync(entry).isDirectory();
} catch (err) {
return false;
}
});

const files = entries.filter(entry => {
try {
return fs.statSync(entry).isFile();
} catch (err) {
return false;
}
});
// localeCompare = take into account rules of system language/region for ordering
// undefined = uses the system default, numeric = regular number sorting, base = ignore case & accents
return entries.sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
);
}


// print each entry on its own line (used for -1 flag)
function printEntries(entries) {
entries.forEach(entry => console.log(entry));
}


async function newLs(directory, oneFlag, allFlag) {
try {
// check if path exists and determine if file or directory
const stats = await fs.stat(directory);

// if a file, just print the name
if (stats.isFile()) {
console.log(directory);
return;
}

// reads directory contents
const entries = await fs.readdir(directory);

// filter out hidden files if no -a flag
const filteredEntries = filterFiles(entries, allFlag);

// sort the entries using the sortEntries helper
const sortedEntries = sortEntries(filteredEntries);

// print entries based on -1 flag
if (oneFlag) {
printEntries(sortedEntries); // one per line
} else {
console.log(sortedEntries.join(" ")); // all on one line, separated by spaces
}
} catch (err) {
console.error(`ls: cannot access '${directory}': ${err.message}`);
}
}
25 changes: 25 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.

8 changes: 8 additions & 0 deletions implement-shell-tools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "implement-shell-tools",
"version": "1.0.0",
"type": "module",
"dependencies": {
"commander": "^14.0.2"
}
}
85 changes: 85 additions & 0 deletions implement-shell-tools/wc/wc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { program } from "commander";
import { promises as fs } from "node:fs";
import process from "node:process";

// configure the CLI program with its name, description, arguments, options, and actions (the help instructions)
program
.name("wc")
.description("An alternative to the 'wc' command")
.argument("<files...>", "The file(s) to count lines/words/bytes")
.option("-l, --lines", "Print the newline counts")
.option("-w, --words", "Print the word counts")
.option("-c, --bytes", "Print the byte counts")
.action(async (files, options) => {
try {
// call newWc for all files
await newWc(files, options)
} catch (err) {
console.error(`Error: ${err.message}`);
}
});

program.parse(process.argv);

//helper function to format string for output
function formatCount(count) {
const paddingStart = 3
return count.toString().padStart(paddingStart);
}


// helper function to print the wc outputs per case
function printWcOutput(lineCount, wordCount, byteCount, file, options, noFlags) {
const parts = [];

if (noFlags || options.lines) parts.push(formatCount(lineCount));
if (noFlags || options.words) parts.push(formatCount(wordCount));
if (noFlags || options.bytes) parts.push(formatCount(byteCount));

parts.push(file);
console.log(parts.join(" "));
}

async function newWc(files, options) {

const noFlags =
!options.lines &&
!options.words &&
!options.bytes;

// set the counts variables
let totalLines = 0;
let totalWords = 0;
let totalBytes = 0;

for (const file of files) {
try {
// read each file into a single text string
const content = await fs.readFile(file, "utf8");

// count lines by splitting on '\n' and subtracting 1 because
// each newline creates an extra array element, so length-1 equals the number of newline characters
const lineCount = content.split("\n").length - 1;

// .filter(Boolean) ensures that falsy values like "" (empty string), null, undefined, 0, false are removed
const wordCount = content.split(/\s+/).filter(Boolean).length;

// calculates the number of bytes the file uses when encoded as UTF-8.
// different than just counting chars as some chars (like emojis, accented letters, etc) take more than 1 byte
const byteCount = Buffer.byteLength(content, "utf8");

// update the count
totalLines += lineCount;
totalWords += wordCount;
totalBytes += byteCount;

printWcOutput(lineCount, wordCount, byteCount, file, options, noFlags);
} catch (err) {
console.error(`Error reading file ${file}: ${err.message}`);
}
}
if (files.length > 1) {
// print the totals as wc does
printWcOutput(totalLines, totalWords, totalBytes, "total", options, noFlags);

Choose a reason for hiding this comment

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

What should happen if I give only a single option, e.g. node wc.js sample-files/1.txt -c? Does this print correctly?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks again @LonMcGregor :) I have spotted my error in the program options configuration and changed the remainder of the file as needed to the long flag names.

}
}