libripgrepの使い方 (ignore, globset編)

# 概要
[ripgrep(rg)](https://github.com/BurntSushi/ripgrep)というツールをご存知でしょうか？
ripgrepは[Rust](https://www.rust-lang.org/)で実装された検索用コマンドで、同じような用途のコマンドとしてはgrepや[The Silver Searcher (ag)](https://github.com/ggreer/the_silver_searcher)、[The Platinum Searcher (pt)](https://github.com/monochromegane/the_platinum_searcher)などがあります。ripgrepは他のツールと比較しても十分高速に動作し、リリースされて以来、私は日々の検索コマンドはripgrepを使い続けています。

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

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

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

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

* [ignore](https://crates.io/crates/ignore) (0.4.4)
* [globset](https://crates.io/crates/globset) (0.4.2)
* [grep](https://crates.io/crates/grep) (0.2.3)
* [grep-searcher](https://crates.io/crates/grep-searcher) (0.1.1)
* [grep-regex](https://crates.io/crates/grep-regex) (0.1.1)
* [grep-printer](https://crates.io/crates/grep-printer) (0.1.1)
* [grep-pcre2](https://crates.io/crates/grep-pcre2) (0.1.2)
* [grep-matcher](https://crates.io/crates/grep-matcher) (0.1.1)
* [grep-cli](https://crates.io/crates/grep-cli) (0.1.1)

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

### 簡単な例
```rust
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)`を実行します。

```rust
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
```

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

```rust
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`にマッチしたファイルだけ表示されます。

```shell-session
$ ./target/release/custom
./
./src
./src/main.rs
```

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

```rust
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`にマッチしなかったファイルだけ表示されます。

```shell-session
$ ./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()`で生成できる`WalkParallel`と[crossbeam-channel](https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-channel)を使います。

```rust
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
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](https://github.com/rust-lang-nursery/glob)が存在しますが、短いパス名の場合はほぼ互角、短いパス名でも複数globの場合や長いパス名の場合はglobより高速に動作するようです。（[パフォーマンス](https://github.com/BurntSushi/ripgrep/tree/master/globset#performance)）

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

### 簡単な例
単一globの例です。

```rust
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);
        }
    }
}
```

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

```shell-session
$ ./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()`をそれぞれ使います。
大文字小文字の区別をなくしたい場合は以下のようなコードになります。

```rust
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.rs`と`baR.rs`どちらにもマッチします。

### 複数glob定義（Globset）を使用した例
`GlobSetBuilder`を使います。下の例の場合は`foo/baz.rs`にはマッチしないことになります。

```rust
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);
        }
    }
}
```

# 参考
* https://blog.burntsushi.net/ripgrep/