rust

libripgrepの使い方 (ignore, globset編)

概要

ripgrep(rg)というツールをご存知でしょうか?
ripgrepはRustで実装された検索用コマンドで、同じような用途のコマンドとしてはgrepやThe Silver Searcher (ag)The Platinum Searcher (pt)などがあります。ripgrepは他のツールと比較しても十分高速に動作し、リリースされて以来、私は日々の検索コマンドはripgrepを使い続けています。

このripgrep、コンポーネントがある程度切り出されておりクレートとして利用することができます。それの総称をlibripgrepと呼んでいます。(たぶん)

少し前にlibripgrepを使ってgoodcを実装しました。goodcはRubyで実装されたgoodcheckという正規表現ベースのlinterをRustで実装し直したもので、非常に高速に動作します。
今回は、goodcでは使わなかったクレートもあったので自分の理解を深める意味も含めてlibripgrepの使い方などを紹介しようと思います。

書いているうちに長くなったので、いくつかに分けて記事にしたいと思います。
今回は ignoreglobsetクレートを紹介します。

libripgrep

libripgrepは以下のクレートから構成されています。
(カッコ内は2018.12.04時点での最新バージョン番号です。)

ignore

ignoreはディレクトリ走査用のクレートです。
globやフィルタ等を設定し走査することができます。このクレート単体でも高速に動作します。

簡単な例

use ignore::Walk;

fn main() {
    for result in Walk::new("./") {
        match result {
            Ok(entry) => println!("{}", entry.path().display()),
            Err(err) => println!("ERROR: {}", err),
        }
    }
}

これはドキュメントにも載っている一番簡単な例で、カレントディレクトリを走査してファイルやディレクトリを表示します。.ignoreファイル、.gitignoreを認識して、フィルタリングして走査します。

カスタム定義した動作で走査する

WalkBuilderを使います。
例えば標準で定義されているフィルタ処理を無効にしたい場合は、以下のような形でstandard_filters(false)を実行します。

use ignore::WalkBuilder;

fn main() {
    let mut builder = WalkBuilder::new("./");
    builder.standard_filters(false);
    for result in builder.build() {
        match result {
            Ok(entry) => println!("{}", entry.path().display()),
            Err(err) => println!("ERROR: {}", err),
        }
    }
}

これで.gitignoreなどを無視してディレクトリ走査します。

他にはoverrides()を使ってフィルタの動作を変えてみます。
カレントディレクトリ以下は以下のようになっているとします。

$ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── custom.rs
    ├── main.rs
    └── parallel.rs

例えば以下のような実装をすると、

use ignore::WalkBuilder;
use ignore::overrides::OverrideBuilder;

fn main() {
    let mut builder = WalkBuilder::new("./");
    let mut oride = OverrideBuilder::new("./src");
    oride.add("main.rs").expect("fail override add");
    builder.overrides(oride.build().expect("fail override build"));
    for result in builder.build() {
        match result {
            Ok(entry) => println!("{}", entry.path().display()),
            Err(err) => println!("ERROR: {}", err),
        }
    }
}

src/main.rsにマッチしたファイルだけ表示されます。

$ ./target/release/custom
./
./src
./src/main.rs

これを以下のようにすると、

fn main() {
    let mut builder = WalkBuilder::new("./");
    let mut oride = OverrideBuilder::new("./src");
    oride.add("!main.rs").expect("fail override add");
    builder.overrides(oride.build().expect("fail override build"));
    for result in builder.build() {
        match result {
            Ok(entry) => println!("{}", entry.path().display()),
            Err(err) => println!("ERROR: {}", err),
        }
    }
}

src/main.rsにマッチしなかったファイルだけ表示されます。

$ ./target/release/custom
./
./Cargo.toml
./src
./src/custom.rs
./src/parallel.rs

設定を上書きして動作を変更することができます。

上記の他にも色々と細かく動作を制御することが可能です。
以下にドキュメントがありますので、実装する際は一度確認することをオススメします。

https://docs.rs/ignore/0.4.4/ignore/struct.WalkBuilder.html

並列で走査したい

WalkBuilder::new(path).build_parallel()で生成できるWalkParallelcrossbeam-channelを使います。

use std::thread;
use ignore::{DirEntry, WalkBuilder};
use crossbeam_channel as channel;

fn main() {
    let (tx, rx) = channel::bounded::<DirEntry>(100);
    let builder = WalkBuilder::new("./");

    // ディレクトリ走査結果受信処理
    let stdout_thread = thread::spawn(move || {
        for dent in rx {
            println!("{}", dent.path().to_str().unwrap());
        }
    });

    // ディレクトリ走査処理
    builder.build_parallel().run(|| {
        let tx = tx.clone();
        Box::new(move |result| {
            use ignore::WalkState::*;

            let _ = tx.send(result.unwrap());
            Continue
        })
    });

    drop(tx);
    stdout_thread.join().unwrap();
}

スレッド数を指定することも可能です。以下変更点です。

diff --git a/ripgreps/ignore-example/src/parallel.rs b/ripgreps/ignore-example/src/parallel.rs
index cdd9aaa..df597c1 100644
--- a/ripgreps/ignore-example/src/parallel.rs
+++ b/ripgreps/ignore-example/src/parallel.rs
@@ -4,7 +4,7 @@ use crossbeam_channel as channel;

 fn main() {
     let (tx, rx) = channel::bounded::<DirEntry>(100);
-    let builder = WalkBuilder::new("/Users/hattori/work");
+    let mut builder = WalkBuilder::new("/Users/hattori/work");

     let stdout_thread = thread::spawn(move || {
         for dent in rx {
@@ -12,7 +12,7 @@ fn main() {
         }
     });

-    builder.build_parallel().run(|| {
+    builder.threads(8).build_parallel().run(|| {
         let tx = tx.clone();
         Box::new(move |result| {
             use ignore::WalkState::*;

globset

globsetはディレクトリ走査用のクレートです。
一つまたは複数のglobの取り回しを簡単におこなえるクレートです。
このクレートもまた単体で高速に動作します。
似た用途のクレートにglobが存在しますが、短いパス名の場合はほぼ互角、短いパス名でも複数globの場合や長いパス名の場合はglobより高速に動作するようです。(パフォーマンス

READMEを見れば大体のイメージはつかめると思いますのが、いくつか使用例を紹介したいと思います。

簡単な例

単一globの例です。

use globset::Glob;

fn main() {
    let glob = Glob::new("*.rs").unwrap().compile_matcher();

    for name in vec!["foo.rs", "foo/bar.rs", "Cargo.toml"] {
        if glob.is_match(name) {
            println!("{} is match", name);
        } else {
            println!("{} is not match", name);
        }
    }
}

実行結果は以下の通りです。

$ ./target/release/globset-example
foo.rs is match
foo/bar.rs is match
Cargo.toml is not match

カスタム定義したglobで動作させる

GlobBuilderを使ってデフォルト動作を変えて動作させることができます。
大文字小文字を区別を制御するにはcase_insensitive()、パス区切りを制御したい場合はliteral_separator()、バックスラッシュのエスケープを制御したい場合はbackslash_escape()をそれぞれ使います。
大文字小文字の区別をなくしたい場合は以下のようなコードになります。

use globset::GlobBuilder;

fn main() {
    let glob = GlobBuilder::new("bar.rs")
        .case_insensitive(true)
        .build().unwrap().compile_matcher();

    for name in vec!["bar.rs", "baR.rs"] {
        if glob.is_match(name) {
            println!("{} is match", name);
        } else {
            println!("{} is not match", name);
        }
    }
}

bar.rsbaR.rsどちらにもマッチします。

複数glob定義(Globset)を使用した例

GlobSetBuilderを使います。下の例の場合はfoo/baz.rsにはマッチしないことになります。

use globset::{Glob, GlobSetBuilder};

fn main() {
    let mut builder = GlobSetBuilder::new();
    // {xx,yy}のようなor表現が使える
    builder.add(Glob::new("foo/{foo,bar}.rs").unwrap());
    builder.add(Glob::new("Cargo.*").unwrap());

    let globs = builder.build().unwrap();
    for name in vec!["foo/foo.rs", "foo/bar.rs", "foo/baz.rs", "Cargo.toml", "Cargo.lock"] {
        if globs.is_match(name) {
            println!("{} is match", name);
        } else {
            println!("{} is not match", name);
        }
    }
}

参考