Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Linda_pp
November 24, 2021

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

Rust LT Online #5 発表資料
https://rust.connpass.com/event/228732/

Linda_pp

November 24, 2021
Tweet

More Decks by Linda_pp

Other Decks in Technology

Transcript

  1. ripgrep をライブラリとして使う
    Twitter: @Linda_pp, GitHub: @rhysd
    Rust LT Online #5 (2021/11/23)

    View Slide

  2. ripgrep とは
    有名な grep 互換のコマンドラインツール.SIMD や
    マルチスレッドを活用し処理を高速化してい
    る..gitignore を見てくれたり出力に色を付けてくれ
    たりなど,ユーザフレンドリーな機能も提供.
    pcre2 対応を除いて,すべて Rust で実装されてお
    り,ライブラリとしても使えるようにうまくモジュール化
    されている.

    View Slide

  3. 活用例
    hgrep (Human-friendly GREP) というツールでファイ
    ル検索に利用.
    検索結果を良い感じにスニペットにまとめて構文ハイ
    ライトして出力するコマンドラインツール.
    - リポジトリ: https://github.com/rhysd/hgrep
    - 紹介記事:
    https://rhysd.hatenablog.com/entry/2021/11/2
    3/211530

    View Slide

  4. TL;DR
    https://github.com/rhysd/hgrep/bl
    ob/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

    View Slide

  5. 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)

    View Slide

  6. 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)

    View Slide

  7. サンプルコード
    https://github.com/rhysd/misc/tree/master/rust/chibigrep
    - ripgrep のうち再帰的にファイルを検索してマッチ結果を
    取得する処理部分だけを使う
    - 特定のテキストを含んだファイルを探してくるユースケー
    スを想定
    - ignore, grep-matcher, grep-searcher の3つの crate を
    使う

    View Slide

  8. ディレクトリの 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();

    View Slide

  9. ディレクトリをマルチスレッドで再帰的に辿る
    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 // 検索を中止する
    }
    })
    });

    View Slide

  10. マッチ結果を受け取る Sink を実装
    use grep_searcher::{Sink, SinkMatch};
    struct SearchSink<'a> {
    tx: &'a Sender>,
    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 {
    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)
    }
    }

    View Slide

  11. マッチ処理を行う 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;
    }
    };

    View Slide

  12. 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();
    }

    View Slide

  13. まとめ
    ● ripgrep は個々の機能ごと複数の crate に分割して実装されている
    ○ ファイルパス検索(ignore)
    ○ マッチ処理(grep-regex, grep-matcher)
    ○ ファイル検索(grep-searcher)
    ○ 結果表示(grep-printer)
    ● 「特定のパターンのテキストを含んだファイルを探してくる」という処理に対してはか
    なり汎用的・簡単に使うことができ,実行効率も良い(マルチスレッド活用・SIMD に
    よる検索実装)

    View Slide