Skip to content

London | Nadika Zavodovska | Module-Tools | Sprint 3 | Implement-Shell-Tools #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
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 * as fs from "fs";
import { program } from "commander";
// For reading file line by line
import * as readline from "readline";
// For handling "*.txt"
import * as glob from "glob";

// Set up program
program
.name("cat command")
.description("Implementing 'cat' command (-n, -b flags) functionality")
.argument("<path...>", "Path")
.option("-n", "Display line numbers")
.option("-b", "Display line numbers only for non-empty lines")
.parse(process.argv)

// Function to print file contents with the flags
async function displayFileContents(filePath, displayLineNumbers = false, displayNonEmptyLineNumbers = false) {
Copy link

Choose a reason for hiding this comment

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

issue: This function works and is readable, but we're loading the same data into memory multiple times in different ways. Can you think of any ways that this could be made more efficient?

Copy link
Author

Choose a reason for hiding this comment

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

@ehwus Alex, thank you for the really useful feedback! Yes, there is a more efficient way, I can use the readline module. I've updated the code to use readline to read the file line by line, which helps prevent loading the entire file into memory at once.

try {
// Create a readline interface to read file line by line
const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});

// Initialise the line number
let lineNumber = 1;

// Process each line
for await (const line of rl) {
if (displayNonEmptyLineNumbers) {
if (line.trim()) {
console.log(` ${lineNumber++} ${line}`);
}
} else if (displayLineNumbers) {
console.log(` ${lineNumber++} ${line}`);
} else {
console.log(line);
}
}
} catch (error) {
console.error(`Error reading directory: ${error.message}`);
}
}

//Get user inputs from command line
const filePaths = program.args;
const displayLineNumbers = program.opts().n;
const displayNonEmptyLineNumbers = program.opts().b;
Copy link

Choose a reason for hiding this comment

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

praise: It's great to assign these options into variables that are more descriptive than the single letter flags, it makes the code much more readable

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for the positive feedback, @ehwus Alex. I'll continue to follow this approach in the future!


const arrayFilePaths = [];

// Loop through the array with paths to handle "*"
for (const filePath of filePaths) {
// if filePath includes "*", use glob and push files to thr array
if (filePath.includes("*")) {
arrayFilePaths.push(...await glob(filePath));
} else {
arrayFilePaths.push(filePath);
}
}

// Handling multiply file processing using Promise.all and map()
await Promise.all(arrayFilePaths.map(filePath => displayFileContents(filePath, displayLineNumbers, displayNonEmptyLineNumbers)));
Copy link

Choose a reason for hiding this comment

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

question: Why is map useful here over forEach?

Copy link
Author

Choose a reason for hiding this comment

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

Thank you, @ehwus Alex, great question. map is used because it returns an array of promises, which is needed for Promise.all to wait for all tasks to complete. forEach doesn't return anything, so it wouldn't work with Promise.all.

76 changes: 76 additions & 0 deletions implement-shell-tools/ls/ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Getting the 'promises' part of the 'fs' module and calling it 'fs' to make it easier to use
import { promises as fs } from "fs";
// Getting the 'resolve' function from 'path' module to turn relative path (from argument) into full path (absolute path)
import { resolve } from "path";
// Importing the 'program' object from 'commander' to help build command-line interfaces, handle user inputs.
import { program } from "commander";
// Importing the 'chalk' module to add colors and styles to text in the console
import chalk from "chalk";
Copy link

Choose a reason for hiding this comment

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

praise: 🌈

Copy link
Author

Choose a reason for hiding this comment

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

Thank you!


// Configure the CLI tool with name, description, arguments, and options
// 'program' is from the Commander package and handles command-line arguments
program
.name("ls command")
.description("Implementing ls command (-1, -a flags) functionality")
.argument("[path]", "Path to list", ".")
.option("-a", "List hidden files, directories")
.option("-1", "List one file, directory per line")
// Read and process command-line arguments, process.argv - an array containing the command-line arguments
.parse(process.argv);

// Function to list contents of a directory.
async function listDirectoryFiles(directoryPath, showHiddenFiles = false, oneFilePerLine = false) {
try {
// Function call from the fs module. It reads contents of a directory, returns an array of file names (files)
let files = await fs.readdir(directoryPath);
// Array stores current directory, parent directory)
let dotDirectories = [];
// Array stores the names of the files and directories
let displayFiles = [];
// Ensure '.', '..' are on the top of list when we list hidden files
if (showHiddenFiles) {
Copy link

Choose a reason for hiding this comment

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

praise: This is a clever way of adding the dot directories

dotDirectories = [".", ".."];
}
// Loop through files and add to displayFiles, including hidden files if -a flag is set
for (const file of files) {
if (!file.startsWith(".") || showHiddenFiles) {
displayFiles.push(file);
}
}
// Sort files without leading dots for hidden files
displayFiles.sort((a, b) => {
const fileNameA = a.startsWith(".") ? a.slice(1) : a;
const fileNameB = b.startsWith(".") ? b.slice(1) : b;
// Compares two filenames, considering numbers and ignoring case sensitivity
return fileNameA.localeCompare(fileNameB, "en", { numeric: true, sensitivity: "base" });
});
// Merging two arrays into one
let sortedFiles = [...dotDirectories, ...displayFiles];

const allFiles = sortedFiles.map(file => {
// Creating the absolute path for the file by combining directoryPath and file name
const absoluteFilePath = resolve(directoryPath, file);
// Check if the file is a directory; if yes, color it blue, otherwise keep it normal.
return fs.stat(absoluteFilePath).then(stats =>
stats.isDirectory() ? chalk.blue.bold(file) : file
);
});
// Array with style directory in bold and blue
const styledAllFiles = await Promise.all(allFiles);
Copy link

Choose a reason for hiding this comment

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

praise: Smart awaiting of Promises here

Copy link
Author

Choose a reason for hiding this comment

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

Thank you!

// Print files either one per line or in a single line separated by spaces
console.log(oneFilePerLine ? styledAllFiles.join("\n") : styledAllFiles.join(" "));
// If an error occurs while reading the directory, console the error message
} catch (error) {
console.error(`Error reading directory: ${error.message}`);
}
}

// Get user inputs from command-line options and arguments
const directory = program.args[0] || ".";
const showHiddenFiles = program.opts().a;
const oneFilePerLine = program.opts()["1"];

// Resolve the absolute path of the directory
const absoluteDirectoryPath = resolve(directory);
// Call the function to list the files in the directory (with options)
await listDirectoryFiles(absoluteDirectoryPath, showHiddenFiles, oneFilePerLine);
Loading