ripgrep をライブラリとして使う
Twitter: @Linda_pp
GitHub: @rhysd
Rust LT Online #5 (2021/11/24)
ripgrep とは
有名な grep 互換のコマンドラインツール.SIMD やマルチスレッドを活用し処理を高速化している..gitignore を見てくれたり出力に色を付けてくれたりなど,ユーザフレンドリーな機能も提供.
pcre2 対応を除いて,すべて Rust で実装されており,ライブラリとしても使えるようにうまくモジュール化されている.
活用例
hgrep (Human-friendly GREP) というツールでファイル検索に利用.
検索結果を良い感じにスニペットにまとめて構文ハイライトして出力するコマンドラインツール.
TL;DR
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
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)
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)
サンプルコード
https://github.com/rhysd/misc/tree/master/rust/chibigrep
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
ディレクトリの 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();
ディレクトリをマルチスレッドで再帰的に辿る
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 // 検索を中止する
}
})
});
マッチ結果を受け取る 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)
}
}
マッチ処理を行う 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;
}
};
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();
}
まとめ