tech. tt

Teens Town 技術ブログ

正規表現でテキストをたくさん抽出する。

Teens Townポータルシステム はじめ、手がける多くのシステムでテストコードを作成しています。そりゃそうか。

完全個人開発ならそれだけで良いのですが、仕事などで時折「テストケースの一覧が欲しい」といわれることがありました。

僕がよく書くテストの命名法は

test "001122. ブログ投稿内容を空で保存" do
  login_action
  visit hoge_path
  click_on "awesome button"
  ...
end

のように、ユニークIDとともにテストの概要を書きます。

これだけならgit grepでいけるのですが、今回は3行目visit hoge_pathのような初回表示ページも必要でした。

そこまでをgit grepでやるのは骨が折れるので自作しました。

github.com

詳細なコード紹介

const figlet = require("figlet");
const chalk = require("chalk");
const glob = require("glob");
const fs = require("fs");
const {
  convertArrayToCSV
} = require("convert-array-to-csv");
const args = process.argv;
if (args.length < 2) {
  console.error("Error!");
  process.exit(1);
}

// 配列をカンマ区切り(CSV)形式の文字列に変換
const convertArrayToTSV = (lines) => {
  let outStr = "";
  for (const line of lines) {
    let tabRemovedLine = line.map(item => item.replace(/\t/g, " "));
    tabRemovedLine = tabRemovedLine.join("\t");
    outStr += (tabRemovedLine + "\n");
  }
  return outStr;
};

// 対象のディレクトリ
let targetDir = process.cwd();
// 許可する拡拡張子
let extensions = [];

let regex = new RegExp();
let regexMain = new RegExp();
let outFlg = "stdout";
let outFileName = "matcher.out";

// ヘルプを表示するかどうか
const helpIdx = args.indexOf("--help");
if (-1 !== helpIdx) {
  figlet("Matcher ML", function (err, data) {
    console.log(data);
    console.log("delevoper: " + chalk.bold.yellow("up-tri") + " " + chalk.underline("https://up-tri.me"));
    console.log();
    console.log(chalk.blue("How to use"));
    console.log();
    console.log("`" + chalk.yellow("node source_matcher_ml.js --dir <target dir> --regex \"regular expressions\" <out flag <file name> >") + "`");
    console.log();
    console.log(chalk.green("[regex]"));
    console.log("Regular expression to get a string (must use grouping)");
    console.log(chalk.green("[out flags]"));
    console.log("--stdout\t...The result is displayed on standard output.");
    console.log("--json\t...The result is saved in JSON.");
    console.log("--csv\t...The result is saved in CSV.");
    console.log(chalk.green("[filename]"));
    console.log("If you set A or B Flag, you can set the output file name.");
    console.log("ex.) " + chalk.yellow("`--csv \"/path/to/savedir/testcase.csv\"`"));
    process.exit(0);
  });
} else {

  // 対象ディレクトリを指定する場合の挙動
  const dirNameIdx = args.indexOf("--dir");
  if (-1 !== dirNameIdx) {
    targetDir = args[dirNameIdx + 1];
  }

  // 拡張子リストを指定する場合
  const extensionIdx = args.indexOf("--ext");
  if (-1 !== extensionIdx) {
    extensions = args[extensionIdx + 1].split(",");
  }

  // 標準出力フラグ
  const stdoutIdx = args.indexOf("--stdout");
  if (-1 !== stdoutIdx) {
    outFlg = "stdout";
  }

  // CSV出力フラグ
  const csvIdx = args.indexOf("--csv");
  if (-1 !== csvIdx) {
    outFlg = "csv";
    outFileName = args[csvIdx + 1] || "matcher.out.csv";
  }

  // JSON出力フラグ
  const jsonIdx = args.indexOf("--json");
  if (-1 !== jsonIdx) {
    outFlg = "json";
    outFileName = args[jsonIdx + 1] || "matcher.out.json";
  }

  // 正規表現指定フラグ
  const regexIdx = args.indexOf("--regex");
  if (-1 !== regexIdx) {
    // GlobalフラグをONにしておく(そうしないと複数取得できない)
    regex = new RegExp(args[regexIdx + 1], "g");
    regexMain = new RegExp(args[regexIdx + 1]);
  } else {
    console.error("Error!");
    process.exit(1);
  }

  // 指定したディレクトリが存在しないときはエラーを返
  if (!fs.existsSync(targetDir)) {
    console.error("Error!");
    process.exit(1);
  }

  let specifiedExtensionString = "";
  switch (extensions.length) {
    case 0:
      break;
    case 1:
      specifiedExtensionString = `.${extensions[0]}`;
      break;
    default:
      specifiedExtensionString = `.{${extensions.join(", ")}}`;
      break;
  }

  // 検索対象ディレクトリに拡張子設定を結合
  let globTarget = targetDir + `/**/*${specifiedExtensionString}`;
  if (fs.lstatSync(targetDir).isFile()) {
    globTarget = targetDir;
  }
  // 検索!
  glob(globTarget, {
    nodir: true
  }, (err, res) => {
    if (err) {
      console.log("Error", err);
    } else {
      const out = [];
      for (const path of res) {
        // 個々のファイルを再帰的に見に行きます
        const content = fs.readFileSync(path).toString();
        const results = content.match(regex);
        if (results !== null) {
          for (const hit of results) {
            // 実行時に入力した正規表現で検索
            let match = hit.match(regexMain);
            delete match["index"];
            delete match["input"];
            delete match["groups"];
            // 先頭には検索文字列が格納されているので捨てる
            match = match.splice(1, match.length);
            out.push(match);
          }
        }
      }
      // 出力設定に合わせて成形
      switch (outFlg) {
        case "stdout":
          console.log(convertArrayToTSV(out));
          break;
        case "csv":
          fs.writeFileSync(outFileName, convertArrayToCSV(out));
          break;
        case "json":
          fs.writeFileSync(outFileName, JSON.stringify(out, {}, 2));
          break;
      }
    }
  });
}

使ってみる。

$ matcher-ml --dir "test/system/" --regex "\"([s0-9]{5,6})\. (.+)\" do\n[\s\S]*?visit (.+)\n" --stdout | pbcopy
  • 本筋の内容ではありませんが、正規表現で\n[\s\S]*?と書くと任意行をすっ飛ばせます。
  • いろいろ出力オプションを用意したけど、結局--stdout | pbcopyとしてスプレッドシートとかにコピペすることが多いですね。計画性!!