Add Functionalities
You need to support these functionalities as shown in the project objectives section:
-c, --count
-i, --ignore-case
-n, --line-number
-r, --recursive
-v, --invert-match
Add dependency yargs
for complex arguments parsing:
npm i yargs
bin/grepjs:
#!/usr/bin/env node
const fs = require("fs");
const yargs = require("yargs");
const { grep, grepCount, grepRecursive } = require("../lib/grep");
// Parse command-line options
const argv = yargs
.usage("Usage: $0 [options] <pattern> <file>")
.option("c", {
alias: "count",
describe: "Only a count of selected lines is written to standard output.",
type: "boolean",
})
.option("h", {
alias: "help",
describe: "Print a brief help message.",
type: "boolean",
})
.option("i", {
alias: "ignore-case",
describe:
"Perform case insensitive matching. By default, it is case sensitive.",
type: "boolean",
})
.option("n", {
alias: "line-number",
describe:
"Each output line is preceded by its relative line number in the file, starting at line 1. The line number counter is reset for each file processed. This option is ignored if -c is specified.",
type: "boolean",
})
.option("r", {
alias: "recursive",
describe: "Recursively search subdirectories listed.",
type: "boolean",
})
.option("v", {
alias: "invert-match",
describe:
"Selected lines are those not matching any of the specified patterns.",
type: "boolean",
})
.demandCommand(2, "Please provide both pattern and file arguments.").argv;
const pattern = argv._[0];
const filePath = argv._[1];
if (argv.help) {
// Print help message and exit
console.log(argv.help);
process.exit(0);
}
const options = {
ignoreCase: argv["ignore-case"],
invertMatch: argv["invert-match"],
};
const result = argv.recursive
? grepRecursive(pattern, filePath, options)
: grep(pattern, filePath, options);
result
.then((result) => {
if (argv.count) {
console.log(grepCount(result));
} else {
printResult(result, argv["line-number"]);
}
})
.catch((error) => {
console.error("Error:", error.message);
});
function printResult(result, showLineNumber) {
let currentFile = null;
const fileCount = Object.keys(result).length;
for (const [filePath, lines] of Object.entries(result)) {
for (const [lineNumber, line] of lines) {
if (fileCount > 1 && filePath !== currentFile) {
currentFile = filePath;
console.log(`\n${filePath}:`);
}
if (showLineNumber) {
console.log(`${lineNumber}: ${line}`);
} else {
console.log(line);
}
}
}
}
yargs
adds all the optional arguments into the app. Function printResult
parses the result object and prints the filePath
and lineNumber
if needed.
lib/grep.js:
const fs = require("fs");
const path = require("path");
async function grep(pattern, filePath, options = {}) {
const { ignoreCase, invertMatch } = options;
const lines = await _readFileLines(filePath);
const regexFlags = ignoreCase ? "gi" : "g";
const regex = new RegExp(pattern, regexFlags);
if (invertMatch) {
matchingLines = _filterLines(regex, lines, false);
} else {
matchingLines = _filterLines(regex, lines, true);
}
return { [filePath]: matchingLines };
}
async function grepRecursive(pattern, dirPath, options = {}) {
let results = {};
try {
const files = await fs.promises.readdir(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const isSubDir = (await fs.promises.stat(filePath)).isDirectory();
const result = !isSubDir
? await grep(pattern, filePath, options)
: await grepRecursive(pattern, filePath, options);
results = { ...results, ...result };
}
} catch (err) {
console.error(err);
}
return results;
}
function grepCount(result) {
return Object.values(result).reduce(
(count, lines) => count + lines.length,
0
);
}
function _filterLines(regexPattern, lines, flag) {
return lines
.map((line, lineNumber) => {
const match = regexPattern.test(line);
return flag === match ? [lineNumber + 1, line.trim()] : null;
})
.filter(Boolean);
}
async function _readFileLines(filePath) {
try {
// Read the file asynchronously
const data = await fs.promises.readFile(filePath, "utf8");
return data.split("\n");
} catch (error) {
console.error("Error reading the file:", error.message);
}
return [];
}
module.exports = {
grep,
grepCount,
grepRecursive,
};
Function grep
adds logic for "case-insensitive match" and "invert match."
Function grepRecursive
lists all files recursively, grep
s them, and puts the results into the big result object.
Run the normal way:
./bin/grepjs filePath lib/grep.js
Result lines:
async function grep(pattern, filePath, options = {}) {
const lines = await _readFileLines(filePath);
return { [filePath]: matchingLines };
const filePath = path.join(dirPath, file);
const isSubDir = (await fs.promises.stat(filePath)).isDirectory();
? await grep(pattern, filePath, options)
: await grepRecursive(pattern, filePath, options);
async function _readFileLines(filePath) {
const data = await fs.promises.readFile(filePath, "utf8");
Do the count:
./bin/grepjs -c filePath lib/grep.js
Result number is: 9.
Show the line numbers:
./bin/grepjs -n filePath lib/grep.js
Result lines:
4: async function grep(pattern, filePath, options = {}) {
6: const lines = await _readFileLines(filePath);
14: return { [filePath]: matchingLines };
22: const filePath = path.join(dirPath, file);
23: const isSubDir = (await fs.promises.stat(filePath)).isDirectory();
25: ? await grep(pattern, filePath, options)
26: : await grepRecursive(pattern, filePath, options);
51: async function _readFileLines(filePath) {
54: const data = await fs.promises.readFile(filePath, "utf8");
Use regex pattern
./bin/grepjs -n "\br[a-z]+t" lib/grep.js
Result lines:
14: return { [filePath]: matchingLines };
18: let results = {};
24: const result = !isSubDir
27: results = { ...results, ...result };
32: return results;
35: function grepCount(result) {
43: return lines
46: return flag === match ? [lineNumber + 1, line.trim()] : null;
55: return data.split("\n");
59: return [];
Do the invert match:
./bin/grepjs -vn result lib/grep.js
Result is something like this:
1: const fs = require("fs");
2: const path = require("path");
3:
4: async function grep(pattern, filePath, options = {}) {
5: const { ignoreCase, invertMatch } = options;
...
8: const regex = new RegExp(pattern, regexFlags);
9: if (invertMatch) {
...
62: module.exports = {
63: grep,
64: grepCount,
65: grepRecursive,
66: };
Do the case insensitive match:
./bin/grepjs -i only bin/grepjs
Result is:
describe: "Only a count of selected lines is written to standard output.",
Do the big recursive match:
./bin/grepjs -r filePath .
Result lines should be like:
bin/grepjs:
const filePath = argv._[1];
? grepRecursive(pattern, filePath, options)
for (const [filePath, lines] of Object.entries(result)) {
if (fileCount > 1 && filePath !== currentFile) {
console.log(`\n${filePath}:`);
lib/grep.js:
async function grep(pattern, filePath, options = {}) {
const lines = await _readFileLines(filePath);
return { [filePath]: matchingLines };
const filePath = path.join(dirPath, file);
const isSubDir = (await fs.promises.stat(filePath)).isDirectory();
? await grep(pattern, filePath, options)
: await grepRecursive(pattern, filePath, options);
async function _readFileLines(filePath) {
const data = await fs.promises.readFile(filePath, "utf8");