Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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}`);
}
}
}
90 changes: 90 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 entries either one per line (-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 for -1 flag (one per line)
printEntries(sortedEntries);

Choose a reason for hiding this comment

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

How does your code differ if the user does not give the -1flag?

Copy link
Author

Choose a reason for hiding this comment

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

Ah thanks for this @LonMcGregor! I hadn't finished formatting the output for not using the -1 flag, oops! Fixed it now :)

} 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.l) parts.push(formatCount(lineCount));
if (noFlags || options.w) parts.push(formatCount(wordCount));
if (noFlags || options.c) parts.push(formatCount(byteCount));

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

async function newWc(files, options) {

const noFlags =
!options.l &&
!options.w &&
!options.c;

// 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.

}
}