正規表現でテキストをたくさん抽出する。
Teens Townポータルシステムはじめ、手がける多くのシステムでテストコードを作成しています。そりゃそうか。
完全個人開発ならそれだけで良いのですが、仕事などで時折「テストケースの一覧が欲しい」といわれることがありました。
僕がよく書くテストの命名法は
test "001122. ブログ投稿内容を空で保存" do
login_action
visit hoge_path
click_on "awesome button"
...
end
のように、ユニークIDとともにテストの概要を書きます。
これだけならgit grep
でいけるのですが、今回は3行目visit hoge_path
のような初回表示ページも必要でした。
そこまでをgit grep
でやるのは骨が折れるので自作しました。
https://github.com/up-tri/source-matcher-ml.js
詳細なコード紹介
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
としてスプレッドシートとかにコピペすることが多いですね。計画性!!