» Node.js创建命令行程序grep » 2. 开发 » 2.3 添加基本功能

添加基本功能

想要达到项目目标,需要实现以下功能:

-c, --count
-i, --ignore-case
-n, --line-number
-r, --recursive
-v, --invert-match

添加 yargs 依赖项以实现复杂参数的解析:

npm i yargs

bin/grepjs:

#!/usr/bin/env node

const fs = require("fs");
const yargs = require("yargs");

const { grep, grepCount, grepRecursive } = require("../lib/grep");

// 解析命令行参数
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) {
  // 打印帮助信息并退出进程
  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 加入所有可选参数的解析。printResult 函数解析结果对象并按需打印 filePathlineNumber

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 {
    // 读取文件
    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,
};

grep 函数添加“不区分大小写匹配”和“反向匹配”逻辑。grepRecursive 函数递归地列出所有文件,执行 grep 操作,然后输出到结果对象中。

常规用法:

./bin/grepjs 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");

计数:

./bin/grepjs -c filePath lib/grep.js

结果数字:9

显示行号:

./bin/grepjs -n filePath lib/grep.js

结果:

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");

使用正则:

./bin/grepjs -n "\br[a-z]+t" lib/grep.js

结果:

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 [];

反向匹配:

./bin/grepjs -vn result lib/grep.js

结果:

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: };

不区分大小写匹配:

./bin/grepjs -i only bin/grepjs

结果:

describe: "Only a count of selected lines is written to standard output.",

递归匹配:

./bin/grepjs -r filePath .

结果:

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");
上页下页