1 of 14

ripgrep をライブラリとして使う

Twitter: @Linda_pp

GitHub: @rhysd

Rust LT Online #5 (2021/11/24)

2 of 14

ripgrep とは

有名な grep 互換のコマンドラインツール.SIMD やマルチスレッドを活用し処理を高速化している..gitignore を見てくれたり出力に色を付けてくれたりなど,ユーザフレンドリーな機能も提供.

pcre2 対応を除いて,すべて Rust で実装されており,ライブラリとしても使えるようにうまくモジュール化されている.

3 of 14

活用例

hgrep (Human-friendly GREP) というツールでファイル検索に利用.

検索結果を良い感じにスニペットにまとめて構文ハイライトして出力するコマンドラインツール.

4 of 14

TL;DR

https://github.com/rhysd/hgrep/blob/main/src/ripgrep.rs

↑ここにすべての実装が入っている

grep 出力パーサ

grep 実装

chunk の計算処理

Printer

Bat Printer

Syntect Printer

↓ ここの話

--no-ignore

--ignore-case

--smart-case

--glob GLOB...

--glob-case-insensitive

--fixed-strings

--word-regexp

--follow

--multiline

--multiline-dotall

--crlf

--mmap

--max-count NUM

--max-depth NUM

--max-filesize NUM+SUFFIX?

--line-regexp

--invert-match

--pcre2

--type TYPE

--type-not TYPE

--type-list

--one-file-system

--no-unicode

--regex-size-limit

--dfa-size-limit

5 of 14

ripgrep のモジュール構造

ripgrep

cli

core

ignore

globpath

matcher

pcre2

regex

memmem

memchr

printer

searcher

ripgrep 用 CLI パーサ (grep-cli)

本体(main 関数はここ)

ディレクトリを辿ってファイルパスを列挙

**/*.rs のような glob のファイルパス列挙

ファイルに対するマッチ処理 (grep-matcher)

pcre2 の Rust binding

有名な regex crate

libc の memmem の SIMD 実装

libc の memchr の SIMD 実装

ファイルの検索処理 (grep-searcher)

マッチ結果の表示処理 (grep-printer)

6 of 14

ripgrep のモジュール構造

ripgrep

cli

core

ignore

globpath

matcher

pcre2

regex

memmem

memchr

printer

searcher

ripgrep 用 CLI パーサ (grep-cli)

本体(main 関数はここ)

ディレクトリを辿ってファイルパスを列挙

**/*.rs のような glob のファイルパス列挙

ファイルに対するマッチ処理 (grep-matcher)

pcre2 の Rust binding

有名な regex crate

libc の memmem の SIMD 実装

libc の memchr の SIMD 実装

ファイルの検索処理 (grep-searcher)

マッチ結果の表示処理 (grep-printer)

7 of 14

サンプルコード

https://github.com/rhysd/misc/tree/master/rust/chibigrep

  • ripgrep のうち再帰的にファイルを検索してマッチ結果を取得する処理部分だけを使う
  • 特定のテキストを含んだファイルを探してくるユースケースを想定
  • ignore, grep-matcher, grep-searcher の3つの crate を使う

8 of 14

chibigrep 処理概要

スレッド1

スレッド2

スレッド3

/path/to/file1

/path/to/file2

/path/to/file3

/path/to/file1を検索

/path/to/file2を検索

/path/to/file3を検索

結果を集計

ignore

grep-matcher

grep-searcher

mpsc

9 of 14

ディレクトリの walker を生成

use ignore::{WalkBuilder, WalkState};

// Path を再帰的に辿る walker を生成.今回は WalkParallel でマルチスレッドでパスを辿る

// スレッドプールは自動で生成される(スレッド数は指定もできるが,デフォルトで良い感じに決めてくれる)

let mut builder = WalkBuilder::new(path);

for path in rest {

builder.add(path);

}

builder

.hidden(false) // 隠しファイルを検索

.ignore(true) // ignore されたファイルを無視

.parents(true); // 親ディレクトリを辿って .gitignore を探す

let walker = builder.build_parallel();

10 of 14

ディレクトリをマルチスレッドで再帰的に辿る

let (tx, rx) = mpsc::channel(); // walker.run はマルチスレッドで呼ばれるので値の受け渡しを channel でやる

walker.run(|| {

// 初期化関数.ここはスレッドプールのスレッドごとに呼ばれる

let tx = tx.clone();

Box::new(move |result| match result {

// この内側のコールバックはファイルパスごとに呼ばれる

Ok(entry) if entry.file_type().map(|t| t.is_file()).unwrap_or(false) => {

// `entry` は `ignore::DirEntry`

grep_file(pat, entry.into_path(), &tx); // ファイルの時.ファイル内を検索

ignore::WalkState::Continue // 検索を続ける

}

Ok(_) => ignore::WalkState::Continue, // ディレクトリの時.検索を続ける

Err(err) => {

tx.send(Err(format!("{}", err))).unwrap();

ignore::WalkState::Quit // 検索を中止する

}

})

});

11 of 14

マッチ結果を受け取る Sink を実装

use grep_searcher::{Sink, SinkMatch};

struct SearchSink<'a> {

tx: &'a Sender<Result<Match, MyError>>,

path: &'a Path,

}

// 結果を集めるためのコールバックを Sink で実装.マッチ箇所ごとに `matched` が呼ばれる

impl<'a> Sink for SearchSink<'a> {

type Error = io::Error;

// `SinkMatch` にマッチ情報が入っている

fn matched(&mut self, _searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {

let m = Match {

path: self.path.to_owned(),

lnum: mat.line_number().unwrap_or(0),

line: mat.bytes().to_vec(),

};

self.tx.send(Ok(m)).unwrap(); // マッチ結果を返す

Ok(true)

}

}

12 of 14

マッチ処理を行う matcher を生成

use grep_regex::RegexMatcherBuilder;

let mut builder = RegexMatcherBuilder::new();

builder

.case_smart(true) // smart case を有効に

.unicode(true); // unicode 対応

// Matcher を生成.今回は regex crate を使った RegexMatcher を使う

// これ以外にも pcre2 を使ったものもある

let matcher = match builder.build(pat) {

Ok(m) => m,

Err(err) => {

tx.send(Err(format!("{}", err))).unwrap();

return;

}

};

13 of 14

searcher を生成してファイル内を検索

use grep_searcher::{BinaryDetection, MmapChoice, SearcherBuilder};

// Searcher を生成

let mut builder = SearcherBuilder::new();

builder

.binary_detection(BinaryDetection::quit(0)) // バイナリファイルだと判明したら検索をやめる

.line_number(true)

.memory_map(unsafe { MmapChoice::auto() }); // mmap を有効にする

let mut searcher = builder.build();

// ここでファイルを検索.マッチごとに sink の matched メソッドが呼ばれる

let mut sink = SearchSink { tx, path: &path };

if let Err(err) = searcher.search_path(&matcher, &path, &mut sink) {

tx.send(Err(format!("{}", err))).unwrap();

}

14 of 14

まとめ

  • ripgrep は個々の機能ごと複数の crate に分割して実装されている
    • ファイルパス検索(ignore)
    • マッチ処理(grep-regex, grep-matcher)
    • ファイル検索(grep-searcher)
    • 結果表示(grep-printer)

  • 「特定のパターンのテキストを含んだファイルを探してくる」という処理に対してはかなり汎用的・簡単に使うことができ,実行効率も良い(マルチスレッド活用・SIMD による検索実装)