LoginSignup
24
19

More than 3 years have passed since last update.

Rust でちょっとしたコマンドラインツールを書いてみた話

Last updated at Posted at 2019-08-04

Logrep

ちょっとしたコマンドラインツールを書きました。
https://github.com/reismannnr2/logrep

得られた知見

条件次第で変化するクロージャを使ったループ処理を書くとき、以下の構成にするとループ内のクロージャ呼び出しを静的ディスパッチできる。

  1. 「クロージャを使ったループ処理」をする構造体を作る。
  2. ループ処理のエントリポイントをトレイト化する。
  3. 条件分岐で1の構造体を返す関数を作成し、戻り値を2のトレイトオブジェクトにする。

経緯

仕事で数十万行のログファイルを漁ることがよくあるのですが、普通にgrepすると以下のように複数行のログのヒット行だけが出力されて大変うれしくない。

[YYYY/MM/DD HH:MM:SS.ZZZ GMT] ERROR SUMMARY LINE
BLA BLA BLA
FOO BAR BAZ
HOGE FUGA PIYO
[YYYY/MM/DD HH:MM:SS.ZZZ GMT] DEBUG SUMMARY LINE
DER DES DEM DEN
DIE DER DER DIE
DAS DES DEM DAS

grep FOO sample.log

FOO BAR BAZ

数行のセットで1単位なので FOO で検索したら以下のように出したい。

[YYYY/MM/DD HH:MM:SS.ZZZ GMT] ERROR SUMMARY LINE
BLA BLA BLA
FOO BAR BAZ
HOGE FUGA PIYO

要件的なもの

もともとの動機が数十MB程度のログをフィルタリングすることだったので、以下のようなイメージで作り始めた。

  • せいぜい数十MB程度までなので、メモリは気にしなくていい
    • 数十MBのテキストを扱いたいので、そこそこ高速化はしておきたい
  • デリミタ行の書式は環境設定とオプションで変更できるようにする
  • 正規表現検索と完全一致検索
  • 単一ファイル以外に、標準入力にも対応
    • パイプで繰り返しフィルタかけられると便利だなーと思ったので
  • 反対に、1行でもマッチ行が存在するブロックを除外するフラグ

苦労した点とか

クロージャの型問題。
Rustのクロージャは静的ディスパッチをするのでとても高速というのは知識として知っていました。

しかしその反面、引数と戻り値が一緒でも、条件によって違う複数のクロージャを同じ変数に入れたりできない。

// 各クロージャの型が違うのでコンパイルエラー
fn get_closure(pattern: &str, use_regex: bool, case_insensitive: bool) -> impl Fn(&str) -> bool {
    match (use_regex, case_insensitive) {
        (true, true) => {
            // エラー処理は今回の本質ではないので省略
            let pattern = Regex::new(&format!("(?i){}", pattern)).unwrap();
            move |line: &str| pattern.is_match(line)
        },
        (true, false) => {
            let pattern = Regex::new(pattern).unwrap();
            move |line: &str| pattern.is_match(line)
        },
        (false, true) => {
            let pattern = pattern.to_lowercase();
            move |line: &str| line.to_lowercase().contains(&pattern)
        },
        (false, false) => {
            |line: &str| line.contains(pattern)
        }
    }
}

クロージャの中で分岐したり、 Box<dyn Fn(&str) -> bool> とかしたりすると、一回のコマンドでは条件は毎回同じなのに、数十万回のループで毎回動的ディスパッチや判定分岐が入ってよろしくない。

一方、 enum とかで分岐するとこれはこれでコードが分岐だらけになって汚くてよろしくない。

解決策:

pub trait Searcher {
    fn search_from_str<'a>(&self, content: &'a str) -> Vec<&'a str>;
}

pub struct<F> BlockSearcher<F>
where F: Fn(&str) -> bool
{
    matcher: F
}

impl <F> Searcher for BlockSearcher<F>
where F: Fn(&str) -> bool
{
    fn search_from_str<'a>(&self, content: &'a str) -> Vec<&'a str> {
        // この中でクロージャ `self.matcher` を使ったループ処理
    }
}

fn get_searcher(pattern: &str, use_regex: bool, case_insensitive: bool) -> Box<dyn Searcher> {
    match (use_regex, case_insensitive) {
        (true, true) => {
            let pattern = Regex::new(&format!("(?i){}", pattern)).unwrap();
            Box::new(BlockSearcher { matcher: move |line: &str| pattern.is_match(line) })
        },
        (true, false) => {
            let pattern = Regex::new(pattern).unwrap();
            Box::new(BlockSearcher { move |line: &str| pattern.is_match(line) })
        },
        (false, true) => {
            let pattern = pattern.to_lowercase();
            Box::new(BlockSearcher { move |line: &str| line.to_lowercase().contains(&pattern) })
        },
        (false, false) => {
            Box::new(BlockSearcher { |line: &str| line.contains(pattern) })
        }
    }
}
let searcher = get_searcher(pattern, true, false);
let result = searcher.search_from_str(text);

こうしておけば汚い分岐は get_searcher に閉じ込めたまま、他の部分は抽象化したコードが書ける。
動的ディスパッチが起こるのは searcher.search_from_str(text) を呼ぶ箇所の一回だけで、ループ内では静的ディスパッチしか行われない、不要な分岐をしないため高速。のはず。

クロージャの所有者をトレイトオブジェクトに閉じ込めて、トレイトメソッドの内部でクロージャを使ったループをすることで、ループ内では静的ディスパッチを維持したまま、型の違うクロージャを1つの変数に入れて取り扱うことができました。

結論

Rustは楽しい。

24
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
19