BurntSushi/ripgrep
github.com/BurntSushi/ripgrep ↗ · 232 files · 3.1 MB
ripgrep (rg) ------------ ripgrep is a line-oriented search tool that recursively searches the current directory for a regex pattern.
Overview
ripgrep is a line-oriented recursive search tool (`rg`) written in Rust that walks a directory tree while respecting `.gitignore`/`.ignore` rules, matches each file with either Rust's `regex` crate or PCRE2, and prints results through one of three printers (standard/summary/JSON). The repository is a Cargo workspace: a thin binary crate at `crates/core` glues together ten reusable library crates (`grep-matcher`, `grep-regex`, `grep-pcre2`, `grep-searcher`, `grep-printer`, `grep-cli`, `ignore`, `globset`, plus the experimental `grep-index`), each published independently on crates.io. What is distinctive is the amount of engineering in the boring parts: a hand-rolled CLI parser over a 246KB table of `&dyn Flag` trait objects that also generates the man page, `--help` text and four shells' completions from the same source of truth; a directory walker that reimplements gitignore precedence with a parallel work-stealing traversal; and a searcher that chooses between memory maps, a reusable line buffer, and multi-line slice scanning per file. An unstable `unstable-index` feature adds a redb+fst ngram index behind `-X/--index` and `--x-crud`.
System map
The workspace root `Cargo.toml` declares the `ripgrep` package producing one binary `rg` from `crates/core/main.rs`, and lists ten workspace members under `crates/`. `crates/core` is not published as a library — it is pure glue plus the CLI definition, as its README states. Dependencies flow strictly downward: core → `grep` facade → `grep-searcher`/`grep-printer`/`grep-regex`/`grep-pcre2`/`grep-cli`/`grep-matcher`, with `ignore` and `globset` used for traversal and glob filtering. There is no network, database or server component in the default build; the only external integrations are subprocesses (decompressors, `--pre` preprocessor, hostname binary) and, under `unstable-index`, a redb embedded database on disk. Deployment is static binaries cross-compiled for ~14 targets by a tag-triggered GitHub Actions release workflow, plus a Debian package described by `[package.metadata.deb]`.
- The single binary target `rg` is declared in the root manifest with `path = "crates/core/main.rs"`, and `autotests = false` with one explicit `[[test]] name = "integration", path = "tests/tests.rs"`.
- `fn main() -> ExitCode` calls `run(flags::parse())` and dispatches on `args.mode()` into seven arms: indexed search, single-threaded `search`, `search_parallel`, `index::write`, `files`/`files_parallel`, `types`, and `generate`.
- The workspace has ten members — `globset`, `grep`, `cli`, `index`, `matcher`, `pcre2`, `printer`, `regex`, `searcher`, `ignore` — sharing `edition = "2024"` and `rust-version = "1.96"` via `[workspace.package]`.
- `crates/grep` is a pure facade that re-exports the constituent crates as `grep::cli`, `grep::matcher`, `grep::printer`, `grep::regex`, `grep::searcher` and (feature-gated) `grep::pcre2`.
- The published crate names differ from directory names: `crates/matcher` is `grep-matcher` v0.1.9, `crates/printer` is `grep-printer` v0.3.1, `crates/searcher` is `grep-searcher` v0.1.17, `crates/regex` is `grep-regex` v0.1.14.
- There is no persistent storage in the default build; the only storage layer is the optional `grep-index` crate, which opens a redb `Database`/`ReadOnlyDatabase` at a file named `index.db` inside a discovered `.ripgrep` directory.
- `grep-index` is marked `publish = false`, version `0.0.1`, and is wired in only through the `unstable-index` Cargo feature (`dep:grep-index`), which the root manifest itself warns "may have very serious bugs".
- Index code is compiled behind a `#[path]` module swap: `crates/core/index/mod.rs` selects `disabled.rs` (which `bail!`s "indexing not enabled in this build") or `enabled.rs` based on the `unstable-index` feature.
- `build.rs` embeds the short git hash as `RIPGREP_BUILD_GIT_HASH` by shelling out to `git rev-parse --short=10 HEAD`, and on `windows`+`msvc` embeds `pkg/windows/Manifest.xml` via `/MANIFEST:EMBED` linker args to enable long paths.
- Build/test commands are `cargo build --release` (optionally `--features pcre2`) and `cargo test --all`; CI additionally runs `cargo test --workspace --features unstable-index` and `cargo fmt --all --check`.
- On musl 64-bit targets the binary swaps in jemalloc as the `#[global_allocator]` because musl's allocator measurably slows ripgrep down; this is the only allocator override.
- Release shape: a `release-lto` profile (fat LTO, `panic = "abort"`, `codegen-units = 1`, symbols stripped) plus `.cargo/config.toml` forcing `+crt-static` for MSVC and musl targets so shipped binaries are statically linked.
Feature catalog
ripgrep's user surface is a single command with a very large flag set: `FLAGS` in `crates/core/flags/defs.rs` is a static slice of `&dyn Flag` trait objects, each carrying short/long/negated/alias names, a documentation category, short and long (mdoc) help text, a completion type, and an `update` method that mutates `LowArgs`. Beyond plain search, the binary has distinct top-level modes — `--files`, `--type-list`, `--generate`, `-h/--help`/`-V/--version` special modes, and the experimental index modes. Output modes (`--count`, `--count-matches`, `-l/--files-with-matches`, `--files-without-match`, `--json`) select among three printers. Filtering features include gitignore semantics, `-t/--type` file types, `-g/--glob` overrides, hidden-file handling and `--max-filesize`. Provider-style integrations are the pluggable regex engine (`--engine auto|default|pcre2`), decompression (`-z`), arbitrary preprocessors (`--pre`), and terminal hyperlinks.
- `Mode` enumerates the five top-level behaviors — `Search(SearchMode)`, `Index(IndexMode)`, `Files`, `Types`, `Generate(GenerateMode)` — with `Mode::Search(SearchMode::Standard)` as the default.
- `SearchMode` has six variants (`Standard`, `FilesWithMatches`, `FilesWithoutMatch`, `Count`, `CountMatches`, `JSON`) and `HiArgs::printer` maps them onto `Printer::Standard`, `Printer::JSON` or `Printer::Summary` with a `SummaryKind`.
- `--generate` emits ancillary artifacts: `GenerateMode::Man` produces roff, and `CompleteBash`/`CompleteZsh`/`CompleteFish`/`CompletePowerShell` produce shell completions, all rendered from the same `FLAGS` table.
- `-h` and `--help` render different documents (`generate_short` vs `generate_long`) from `template.short.help` / `template.long.help`, interpolating per-category placeholders like `!!filter!!` and `!!output-modes!!`.
- `--type-list` is implemented by `types()` in main, which iterates `args.types().definitions()` and prints `name: glob, glob` lines, exiting 1 if there are no definitions.
- ripgrep ships a large built-in file-type table, `DEFAULT_TYPES`, mapping type names to glob lists (e.g. `ada` → `*.adb`,`*.ads`), extensible per-invocation via `--type-add`.
- Three regex engine choices are supported: `EngineChoice::Default` (Rust regex), `PCRE2`, and `Auto`, where `Auto` tries Rust first and falls back to PCRE2, reporting both errors side by side if both fail.
- `-z/--search-zip` transparently decompresses by file extension using external binaries: gzip, bzip2, xz (also for `.lzma`), lz4, brotli, zstd and `uncompress`, keyed by globs like `*.gz`, `*.tgz`, `*.zst`, `*.Z`.
- `--pre` runs an arbitrary user program per file with the path as argv[1] and the file on stdin, optionally restricted by `--pre-glob` via `preprocessor_globs`.
- Hyperlink support is a first-class feature with named aliases (e.g. `"vscode".parse::<HyperlinkFormat>()`) and interpolation variables including `{host}` and a `{wslprefix}` derived from `WSL_DISTRO_NAME`.
- Sorting is user-selectable across four criteria — `path`, `modified`, `accessed`, `created` — with reverse variants; `HiArgs::sort` special-cases ascending path sort to be done during traversal instead of by buffering.
- The experimental indexing surface consists of `--x-crud` (create/update/remove index entries), `--x-index-force` and `--x-index-path`, all in the `Category::Indexing` group, which renders as "Indexing is not supported in this build of ripgrep." when the feature is off.
Data flow
Input enters at three boundaries: `std::env::args_os()`, an optional rc file named by `RIPGREP_CONFIG_PATH`, and stdin (patterns via `-f -`, or the haystack itself). The parser folds all of these into a flat, dumb `LowArgs` struct through `Flag::update`, then `HiArgs::from_low_args` performs all the "expensive" derivations — compiling globs, building the type matcher, resolving the CWD and hostname, choosing thread count and mmap strategy. `HiArgs` is the single state owner for the rest of the process; every downstream object (`ignore::WalkBuilder`, `grep::searcher::Searcher`, the matcher, the printer, the `SearchWorker`) is constructed from it by a dedicated accessor. Bytes then flow file → `Searcher` → `Sink` → printer → `termcolor` writer, either directly to stdout (single-threaded) or into per-thread `termcolor::Buffer`s flushed by a `BufferWriter` (parallel). Exit status is a fourth output channel, driven by a match flag plus a global `ERRORED` atomic.
- Config args are read from the file named by `RIPGREP_CONFIG_PATH`, one shell argument per line with `#` comments and blank lines skipped, then **prepended** to `std::env::args_os().skip(1)` and the whole CLI is re-parsed from scratch.
- `--no-config` short-circuits before the config file is read, and `-h`/`-V` short-circuit even earlier via `ParseResult::Special`, so help output never depends on the config file or the CWD.
- Each flag writes into `LowArgs` — a flat struct of ~60 public(crate) fields — via `Flag::update(FlagValue, &mut LowArgs)`, and by convention `update` only validates, deferring side effects (e.g. resolving `--hostname-bin`) to the high-level stage.
- Patterns are drained from `LowArgs` by `Patterns::from_low_args`, which promotes the first positional to a pattern only when neither `-e` nor `-f` was given, reads `-f -` through `grep::cli::patterns_from_stdin()`, and de-duplicates patterns to avoid pathological regex compile costs.
- `Paths::from_low_args` runs after patterns so all remaining positionals are paths; with none given it uses `grep::cli::is_readable_stdin()` as a heuristic to choose between searching `./` and searching `-` (stdin), and errors if stdin would be consumed twice.
- `State` tracks one-shot derived facts across the low→high conversion, notably `stdin_consumed`, which is what makes the double-stdin error and the CWD heuristic consistent.
- Thread count is derived once in `from_low_args`: forced to 1 when `--sort` is set or only one file is searched, else `--threads`, else `available_parallelism()` capped at 12.
- Memory-map policy is decided from the path set: `MmapMode::Auto` only enables mmap when ≤10 paths are given and all of them are files; otherwise `MmapChoice::never()`.
- Traversal output is `Result<ignore::DirEntry, ignore::Error>`, converted by `HaystackBuilder::build_from_result` into `Option<Haystack>`; errors are reported through `err_message!` and non-file, non-explicit entries are dropped with a debug log.
- `SearchWorker::search` selects among four data sources per haystack in priority order: stdin lock, preprocessor subprocess, decompression subprocess, then direct `search_path`.
- Search results reach printers only through the `Sink` trait (`matched`, `context`, `context_break`, `binary_data`, `begin`, `finish`); `search_path`/`search_reader` build a `sink_with_path` per printer variant and harvest `has_match()` and `stats()` from it.
- In parallel mode each worker owns a cloned `SearchWorker` writing into its own `termcolor::Buffer`; the buffer is cleared before each file and printed atomically via `bufwtr.print(...)` to avoid interleaving.
Behavior traces
Three flows dominate: startup/arg-parsing, a default recursive search, and the file-listing path. All of them funnel through `flags::parse()` → `HiArgs` and then diverge on `Mode`. The parallel search path is notable because parallelism lives entirely in the directory walker — `search_parallel` just hands a closure to `WalkParallel::run` and never manages threads itself. The indexed path currently degrades gracefully: `index::read` opens the index and then falls straight through to the ordinary exhaustive search.
- Startup order: `main` → `flags::parse()` → `parse_low()` which first calls `Logger::init()`, then `Parser::new().parse(env::args_os().skip(1), &mut low)`, then `set_log_levels(&low)` so `--trace` is live before config parsing.
- If a config file yields arguments, the parser discards the first `LowArgs`, builds a fresh `LowArgs::default()`, re-parses `config_args ++ argv`, and re-runs `set_log_levels` because logging flags may have changed.
- `Parser::new()` returns a `&'static Parser` memoized in a `OnceLock`, so the flag-name trie (`FlagMap` over short names, long names, aliases and negations) is built exactly once per process even though `lookup()` calls it repeatedly.
- `HiArgs::from_low_args` begins by asserting `low.special == None` ("special mode demands short-circuiting"), which encodes the invariant that help/version never reach high-level conversion.
- Default search trace: `run` → `search(args, mode)` → `args.walk_builder()?.build()` → `haystack_builder.build_from_result` → `args.sort(...)` → `args.search_worker(args.matcher()?, args.searcher()?, args.printer(mode, args.stdout()))` → loop `searcher.search(&haystack)`.
- Inside one file's search: `SearchWorker::search` sets binary detection (explicit vs implicit) on the `Searcher`, then `search_path` dispatches on the `PatternMatcher` enum to the generic `search_path(m, searcher, printer, path)`, which calls `p.sink_with_path(&matcher, path)` and `searcher.search_path`.
- `Searcher::search_path` → `search_file_maybe_path` first tries `self.config.mmap.open(file, path)` and searches the resulting slice; only if mmap is unavailable does it fall back to the incremental reader path.
- The searcher has three internal strategies implemented in `glue.rs`: `ReadByLine` (streaming reader), `SliceByLine` (whole-slice, e.g. mmap) and `MultiLine` (when `--multiline` is on).
- Parallel search trace: `search_parallel` creates a `BufferWriter`, an `AtomicBool` matched/searched pair and an optional `Mutex<Stats>`, then `args.walk_builder()?.build_parallel().run(|| Box::new(move |result| ...))` clones a `SearchWorker` per thread.
- Early exit under `-q/--quiet` is implemented twice: the sequential loop `break`s when `matched && args.quit_after_match()`, and the parallel closure returns `WalkState::Quit`, which aborts the whole traversal.
- Broken pipe is handled at three levels: inside the search loop (`break`), inside `bufwtr.print` (`WalkState::Quit`), and finally in `main`, which walks `err.chain()` for an `io::Error` of kind `BrokenPipe` and returns exit code 0.
- Preprocessor trace: `search_preprocessor` builds `Command::new(bin).arg(path).stdin(Stdio::from(File::open(path)?))`, wraps it in a `CommandReader`, searches the reader, then calls `rdr.close()` so a non-zero exit code surfaces stderr as a Rust error.
Testing signals
Testing is two-tier and unusually thorough for a CLI. Integration tests live in `tests/` behind a single `[[test]] integration` target and drive the real `rg` binary in a temp directory via a `Dir`/`TestCommand` harness; the `rgtest!` macro automatically runs every test twice — once with the default engine and once with PCRE2 — when the `pcre2` feature is on. Unit tests live inline in `#[cfg(test)] mod tests` blocks, heavily concentrated in `crates/core/flags/defs.rs` (116 `#[test]`s, mostly per-flag parse assertions) and `crates/printer/src/standard.rs` (74). CI runs the suite on 18 build configurations including cross-compiled big-endian (s390x, powerpc64) and 32-bit targets, plus rustfmt, `cargo doc` with `-D warnings`, a wasm32-wasip1 build check, and a cargo-fuzz compile check. There is no coverage measurement and no benchmark job; `benchsuite/` is a separate Python/shell harness not run by CI.
- The integration suite is one binary: `tests/tests.rs` declares modules `binary`, `feature`, `json`, `misc`, `multiline`, `regression`, plus `index` gated on `#[cfg(feature = "unstable-index")]`.
- Test volume by area: `regression.rs` has 110 `rgtest!` cases, `misc.rs` 95, `feature.rs` 82, `binary.rs` 29, `json.rs` 9, `multiline.rs` 8 — regression tests are the single largest group.
- `rgtest!` runs each test body twice when built with the `pcre2` feature — once via `util::setup` and once via `util::setup_pcre2` — which is why CI skips PCRE2 tests under cross/qemu to avoid doubling runtime.
- `Dir::new` locates the `rg` binary relative to `env::current_exe()` and creates an isolated temp working directory keyed by a global atomic counter, so tests can create real files and symlinks.
- `TestCommand::env` exists specifically so tests can exercise environment-driven behavior such as `RIPGREP_CONFIG_PATH`.
- Decompression is covered by committed fixture files: `tests/data/` contains `sherlock.{gz,bz2,xz,lz4,lzma,br,zst,Z}` matching the glob table in `default_decompression_commands`.
- `cmd_exists` lets tests skip gracefully when an external decompression binary isn't installed on the runner, by probing `program --help`.
- Index tests are split into `tests/index/basic.rs` (UX happy paths) and `tests/index/disallowed.rs`, the latter asserting that `-X` combined with `--files-without-match`, `--text` or `-Eutf16` errors out.
- `crates/core/flags/defs.rs` carries 116 `#[test]` functions — largely `parse_low_raw([...])` assertions per flag — making the flag table the most densely unit-tested file in the repo.
- Structural invariants of the flag table are enforced by tests: `shorts_no_duplicates`, `longs_no_duplicates`, `shorts_all_ascii_alphanumeric` and `longs_all_ascii_alphanumeric` (which also validates aliases and negations).
- CI runs two informational tests with `--nocapture`: `flags::defs::tests::available_shorts` (prints unused short-flag letters) and grep-cli's `print_hostname`.
- Shell completion correctness is tested outside cargo by the `ci/test-complete` script, which is skipped on Windows and on cross-compiled targets.
Risk map
ripgrep has a small but real attack surface for a search tool: it executes external programs (decompressors, `--pre`, `--hostname-bin`), reads a config file from an environment-variable-named path whose contents become command-line flags, and memory-maps files it does not control. The codebase acknowledges most of these explicitly in comments — the mmap `unsafe` block calls its own reasoning "suspect", and `resolve_binary` exists specifically to stop Windows CWD-relative program resolution. The most fragile coupling is the 246KB `defs.rs` flag table, where a single flag touches parsing, help text, man page and four completion generators. The experimental index path is the least-finished area: `grep-index` opens with `#![allow(warnings)]`, its literal module is self-described as a prototype, and `index::read` doesn't actually use the index yet.
- Arbitrary code execution is by design in three places — `--pre` (`Command::new(bin).arg(path)`), `-z` decompressors, and `--hostname-bin` — all spawned with the user's environment inherited.
- `resolve_binary` is a deliberate Windows security mitigation: without it, `Command::new` hands relative paths to `CreateProcess`, which searches the CWD, so `rg -z` in an untrusted tree could run an attacker-planted `gzip.exe`. On non-Windows it is a no-op returning the path unchanged.
- `RIPGREP_CONFIG_PATH` turns a file's contents into command-line arguments that are prepended to argv, so anything that can set that env var or write that file can inject flags including `--pre` — a full code-execution vector with no allowlist or validation.
- The memory-map `unsafe` block carries an explicit hazard note: a file truncated concurrently with the search can raise SIGBUS and abort the process, and the author writes "This reasoning should be treated as suspect."
- File IO is unbounded outside the repo root by nature — `walk_builder` starts at user-supplied paths with `follow_links(self.follow)` and `same_file_system(self.one_file_system)`, so `-L` on a hostile tree can be walked out of the intended subtree.
- Index discovery reads `RIPGREP_INDEX_PATH` and otherwise walks every ancestor of the CWD looking for `.ripgrep`, a lookup the code itself flags as "subject to TOCTOU problems".
- `crates/index/src/lib.rs` opens with `#![allow(warnings)]`, suppressing all compiler lints for the entire index crate — including dead code and unused results.
- `crates/index/src/literal.rs` is self-described as "currently a prototype ... hastily written in order to bootstrap ngram extraction", so the ngram query construction (`GramQuery::And/Or/Literal`) is not production-hardened.
- Indexed search is effectively dead weight today: `index::read` binds the opened index to `let _index` and runs a full exhaustive search anyway, so `-X` costs index-open time for zero pruning benefit.
- `crates/core/flags/defs.rs` is 246KB — larger than the next two biggest files combined — and each flag must stay consistent across `Flag::update`, `doc_short`, `doc_long`, `doc_category` and `completion_type`, making it the highest blast-radius file for CLI changes.
- `FlagValue::unwrap_switch`/`unwrap_value` panic via `unreachable!` on mismatch; this is intentional (a bug indicator) but means an `is_switch()`/`update()` inconsistency in a new flag is a runtime panic rather than a compile error.
- Error reporting is global mutable state: three `AtomicBool`s (`MESSAGES`, `IGNORE_MESSAGES`, `ERRORED`) set once at startup and read at exit, so any code path that prints an error without going through `err_message!` silently fails to change the exit code.
Extension points
ripgrep's extension seams are almost all static tables and traits rather than runtime plugin systems. Adding a flag means adding one unit struct implementing `Flag` and one entry in the `FLAGS` slice; everything downstream (parser trie, `-h`, `--help`, man page, four shell completions) derives from that. Adding a file type means one line in `DEFAULT_TYPES`. Adding a decompressor means one `add(glob, ARGS, &mut cmds)` line. At the library level, `Matcher` and `Sink` are the two real trait-based extension points: any regex engine implementing `grep_matcher::Matcher` can be plugged into the searcher, and any output format implementing `grep_searcher::Sink` can consume results. The `ParallelVisitorBuilder` trait in `ignore` lets external callers drive the parallel walker with their own per-thread state.
- The primary extension point is the `Flag` trait plus the `FLAGS: &[&dyn Flag]` slice; the module doc states the `defs` module "provides a single global slice of `&dyn Flag` values corresponding to all of the flags in ripgrep" used via dynamic dispatch.
- One `Flag` impl can expose four user-visible names — short, long, `name_negated()` and an arbitrary `aliases()` list — as with `-E`/`--encoding`/`--no-encoding` sharing a single implementation.
- `Category` (Input, Search, Filter, Output, OutputModes, Indexing, Logging, OtherBehaviors) is the grouping key for generated docs; `Category::as_str` returns the template variable name interpolated into help and man templates.
- `CompletionType` (Other, Filename, Executable, Filetype, Encoding) is the hook a new flag uses to tell all four shell completion generators what to complete.
- Four completion generators are registered in `flags::mod` as `generate_complete_bash/zsh/fish/powershell` and surfaced through `GenerateMode`; adding a shell means adding a module under `flags/complete/` and a `GenerateMode` variant.
- `grep_searcher::Sink` is the output extension trait — implement `matched`, `context`, `context_break`, `binary_data`, `begin`, `finish` — and the crate ships a `sinks` submodule of closure-based conveniences (e.g. `UTF8`) for quick use.
- `grep_matcher::Matcher` is the regex-engine extension trait; `grep-regex` and `grep-pcre2` are two implementations, and `PatternMatcher` in core is the enum that selects between them at runtime.
- Printers are extended by adding a `Printer<W>` enum variant plus a `printer_*` builder in `HiArgs`; each variant only needs `get_mut()` and a `sink_with_path` method for `search_path`/`search_reader` to work.
- New file types plug into `DEFAULT_TYPES`, a `&[(&[&str], &[&str])]` table of (names, globs) that the doc comment explicitly invites PRs against; users extend it per-invocation with `TypesBuilder::add`/`add_def`.
- New decompressors plug into `default_decompression_commands()` via a single `add(glob, ARGS_X, &mut cmds)` call; unavailable binaries are silently skipped after `resolve_binary` fails with a debug log.
- `ignore::ParallelVisitorBuilder` and `WalkParallel::visit` let external code supply per-thread visitors with owned state, as an alternative to the closure-based `run`.
- `WalkBuilder::filter_entry` and `add_custom_ignore_filename` are the per-caller filtering hooks; ripgrep itself uses the latter to register `.rgignore`.
Contributor handoff
Read `crates/core/main.rs`, `crates/core/flags/mod.rs` and `crates/core/flags/hiargs.rs` first — they define the whole control flow and the low→high argument split that every other decision hangs off. Formatting is enforced (`rustfmt.toml`: `max_width = 79`, `use_small_heuristics = "max"`, edition 2024) and CI fails on `cargo fmt --all --check`, so run rustfmt before proposing a diff. Most changes that touch the CLI require edits in three coordinated places: the `Flag` impl, the `FLAGS` slice, and the corresponding `LowArgs`/`HiArgs` fields. Note the repository's `AI_POLICY.md`: autonomous agents are explicitly barred from contributing upstream, and AI-written comments to maintainers may be hidden — treat this repo as read/analyze-friendly but do not open PRs or issues on it autonomously.
- `AI_POLICY.md` states "Autonomous agents are not allowed to be used for contributing to this project" and that PRs appearing in violation will be closed, possibly without notice; `CONTRIBUTING.md` says contributions not following it will be closed.
- Read first, in order: `crates/core/main.rs` (dispatch), `crates/core/flags/mod.rs` (the `Flag` trait and `Category`), `crates/core/flags/hiargs.rs` (`from_low_args` and every builder accessor), `crates/core/search.rs` (`SearchWorker`).
- The core crate's own README warns it is not a library and is "glue code that brings the grep-matcher, grep-regex, grep-searcher and grep-printer crates together" — behavior changes usually belong in a `crates/*` library, not in core.
- Formatting convention is strict and narrow: `max_width = 79`, `use_small_heuristics = "max"`, `edition = "2024"`; the `rustfmt` CI job runs `cargo fmt --all --check`.
- Adding a flag requires: a unit struct implementing `Flag` (with `doc_long` in mdoc format), an entry in `FLAGS` at the right position for doc ordering, a field in `LowArgs`, and usually a matching field/accessor in `HiArgs`.
- Per-flag tests use `parse_low_raw([...])`, a `#[cfg(test)]`-only helper that runs the parser once with no logging and no config file — the right tool for asserting flag parsing without touching the environment.
- By convention `Flag::update` must only validate, never act: the trait docs use `--hostname-bin` as the example of work that must be deferred so it happens once, later.
- Tests to run: `cargo test --all` for the full suite, `cargo test --features pcre2` to double-run integration tests under PCRE2, `cargo test --workspace --features unstable-index` for index tests, and `ci/test-complete` for zsh completions.
- Integration tests create real files in a temp dir and run the built `rg` binary — they are not hermetic against the host, and `cmd_exists` gating means missing `gzip`/`xz` silently reduces coverage rather than failing.
- Trap: `Flag::name_long` must be ≥2 ASCII alphanumeric/`-` characters and short flags must be unique ASCII alphanumeric or `.`; violating either fails `longs_all_ascii_alphanumeric`/`shorts_no_duplicates` rather than failing to compile.
- Trap: changing thread-count derivation can trip `assert_eq!(1, self.threads, "sorting implies single threaded")` in `walk_builder`, because `--sort` and parallelism are mutually exclusive by construction.
- Trap: in parallel mode the printer must not own the file separator — `printer_standard` only calls `separator_search` when `self.threads == 1`, because the `BufferWriter` owns separator logic otherwise.
Languages
| Language | Files | Share of source |
|---|---|---|
| Rust | 110 | 85% |
| Markdown | 23 | docs/config |
| TOML | 14 | 11% |
| YAML | 5 | docs/config |
| Shell | 3 | 2% |
| Lua | 1 | 1% |
| Ruby | 1 | 1% |
Start reading here
Probable source entrypoint: Cargo.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
crates/ (147 files) pkg/ (3 files)
Source layout — crates/
| Path | Files |
|---|---|
crates/cli/ | 12 |
crates/core/ | 30 |
crates/globset/ | 11 |
crates/grep/ | 7 |
crates/ignore/ | 19 |
crates/index/ | 7 |
crates/matcher/ | 9 |
crates/pcre2/ | 7 |
crates/printer/ | 17 |
crates/regex/ | 13 |
crates/searcher/ | 15 |
Repository structure
| Top-level path | Files |
|---|---|
crates/ | 147 |
benchsuite/ | 30 |
tests/ | 19 |
.github/ | 6 |
ci/ | 4 |
fuzz/ | 4 |
pkg/ | 3 |
scripts/ | 1 |
Docs found: CONTRIBUTING.mdREADME.md
Package
Cargo.toml — integration v15.2.0" #:version, 14 declared dependencies.
anyhowbstrgrepgrep-indexignorelexoptlogserdeserde_deriveserde_jsontermcolortextwrap
CI & config entrypoints
README.md— Repository readme.github/workflows/release.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflow
Structural health signals
Average score 7.1/10 across 93 analyzed source files; 32 hotspots surfaced.
crates/ignore/src/walk.rs— 2375 lines, score 4 (Large file, Deep nesting)crates/ignore/src/incremental.rs— 1286 lines, score 4 (Large file, Deep nesting)crates/ignore/src/gitignore.rs— 885 lines, score 4.5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
3fce3b5bb0232026-08-04 — ignore-0.4.335055264ae0212026-08-04 — globset-0.4.20020687a77d132026-08-04 — ignore,globset: increase pool capacity7525479a95762026-08-03 — ci: fix binary discovery5ed408e17ecc2026-08-03 — ignore-0.4.32