CLOVER🍀

That was when it all began.

Node.jsで、grepもどきを書いてみる

Node.jsを使ったコードの練習ということで。

お題として、簡単なgrepコマンドっぽいものを書いてみます。こういう動作設定で書いてみましょう。

  • 起動引数は2つ
  • ひとつは、grep対象のファイルパス
  • もうひとつは、文字列パターン(正規表現)
  • 作成にあたって利用するのは標準APIのみ

まあ、ありきたりな感じですね。

これを前提として、作ってみましょう。対象のNode.jsのバージョンは、こちらとします。

$ node -v
v8.5.0

まずは正攻法的な感じでいくと、File SystemのreadFileを使うのが正しいでしょうか?

fs.readFile

書いてみたのは、こちら。
grep.js

const fs = require("fs");

const args = process.argv.slice(2);
const filePath = args[0];
const pattern = args[1];

fs.readFile(filePath, "utf8", (err, data) => {
    if (err) throw err;

    const lines = data.split(/\r?\n/);
    const size = lines.length;
    const maxWidth = size.toString().length;

    lines
        .map((line, index) => [(index + 1).toString().padStart(maxWidth, " "), line])
        .filter(lineWithIndex => lineWithIndex[1].search(pattern) > -1)
        .forEach(lineWithIndex => {
            console.log(`${lineWithIndex[0]}: ${lineWithIndex[1]}`);
        });
});

FileSystem#readLineだと、ファイルの中身が全部渡ってくるようなので、ここではsplitしてから配列内をパターンにマッチするかどうかテストすることにしました。

    const lines = data.split(/\r?\n/);
    const size = lines.length;
    const maxWidth = size.toString().length;

    lines
        .map((line, index) => [(index + 1).toString().padStart(maxWidth, " "), line])
        .filter(lineWithIndex => lineWithIndex[1].search(pattern) > -1)
        .forEach(lineWithIndex => {
            console.log(`${lineWithIndex[0]}: ${lineWithIndex[1]}`);
        });

データの全量がわかっているので、行番号も付けてみました。

試してみましょう。

$ node grep.js grep.js con.+
 1: const fs = require("fs");
 3: const args = process.argv.slice(2);
 4: const filePath = args[0];
 5: const pattern = args[1];
10:     const lines = data.split(/\r?\n/);
11:     const size = lines.length;
12:     const maxWidth = size.toString().length;
18:             console.log(`${lineWithIndex[0]}: ${lineWithIndex[1]}`);

OKですね。

とはいえ、この方法だとファイルの中身を一気に全部読んでしまう(?)というか、行単位には扱えないので、このあたりが
どうにかならないかな?と思ってちょっと調べたらReadlineを使うとできそうな感じです。

Readline | Node.js v10.9.0 Documentation

Readlineを使った例は、こちら。
readline-grep.js

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

const args = process.argv.slice(2);
const filePath = args[0];
const pattern = args[1];

const readStream = fs.createReadStream(filePath, { encoding: "utf8" });
const rl = readline.createInterface({ input: readStream });

let lineIndex = 0;
rl.on("line", line => {
    lineIndex++;

    if (line.search(pattern) > -1) {
        console.log(`${lineIndex}: ${line}`);
    }
});

ReadlineにはStreamを渡す必要があるみたいなので、FileSystemのcreateReadStreamでReadable Streamを作成します。
fs.createReadStream

こちらをReadlineのcreateInterfaceの入力として設定します、と。
readline.createInterface

この例では、特に行番号はフォーマットしません。

試してみましょう。

$ node readline-grep.js grep.js con.+
1: const fs = require("fs");
3: const args = process.argv.slice(2);
4: const filePath = args[0];
5: const pattern = args[1];
10:     const lines = data.split(/\r?\n/);
11:     const size = lines.length;
12:     const maxWidth = size.toString().length;
18:             console.log(`${lineWithIndex[0]}: ${lineWithIndex[1]}`);

$ node readline-grep.js readline-grep.js con.+
1: const fs = require("fs");
2: const readline = require("readline");
4: const args = process.argv.slice(2);
5: const filePath = args[0];
6: const pattern = args[1];
8: const readStream = fs.createReadStream(filePath, { encoding: "utf8" });
9: const rl = readline.createInterface({ input: readStream });
16:         console.log(`${lineIndex}: ${line}`);

はい。

それにしても、慣れないですねぇ…。