Documentation

grep and ripgrep passthrough

greppy accepts ordinary grep invocations. They run the real system grep and pass its stdout, stderr and exit code through byte for byte. They open no index, load no model and change no greppy cache.

greppy -rn deny_unknown_fields serde_derive/src
greppy -rin "todo" serde_derive/src
greppy grep -rn deny_unknown_fields serde_derive/src

Any invocation that is not one of greppy's commands is a grep invocation. A leading grep word works too: greppy grep -rn foo . and greppy -rn foo . are equivalent.

Byte for byte

cmp compares greppy's output with the output of the system grep on serde 1.0.229:

$ cmp <(greppy -rn deny_unknown_fields serde_derive/src) <(grep -rn deny_unknown_fields serde_derive/src) && echo identical
identical
$ greppy -rn deny_unknown_fields serde_derive/src
serde_derive/src/internals/symbol.rs:13:pub const DENY_UNKNOWN_FIELDS: Symbol = Symbol("deny_unknown_fields");
serde_derive/src/internals/attr.rs:158:    deny_unknown_fields: bool,
serde_derive/src/internals/attr.rs:241:        let mut deny_unknown_fields = BoolAttr::none(cx, DENY_UNKNOWN_FIELDS);
…

The same holds for stderr and the exit code. These ten invocations on serde were each checked with cmp against the original:

Invocation Exit code stdout stdout and stderr
greppy -rn deny_unknown_fields serde_derive/src 0 1,700 bytes identical
greppy grep -rn deny_unknown_fields serde_derive/src 0 1,700 bytes identical
greppy -c -r Content:: serde/src 0 128 bytes identical
greppy -rin 'todo' serde_derive/src 0 118 bytes identical
greppy -rn no_such_identifier_xyz serde/src 1 0 bytes identical
greppy -n deny_unknown_fields does/not/exist.rs 2 0 bytes, 51 bytes stderr identical
greppy -n Serialize < serde_derive/src/ser.rs 0 7,514 bytes identical
greppy rg -n --sort path deny_unknown_fields serde_derive/src 0 1,700 bytes identical to rg
greppy --smart-case -t rust --sort path -n deny_unknown_fields serde_derive 0 1,800 bytes identical to rg
greppy rg -g '!test_suite' --sort path -n 'fn deserialize_struct' . 0 253 bytes identical to rg

greppy also accepts a leading egrep, fgrep or rgrep word and runs the plain grep with it, without adding -E, -F or -r. You pass these flags yourself:

$ greppy egrep 'RENAME_ALL|RENAME_ALL_FIELDS' serde_derive/src/internals/symbol.rs; echo "exit $?"
exit 1
$ greppy egrep -E 'RENAME_ALL|RENAME_ALL_FIELDS' serde_derive/src/internals/symbol.rs
pub const RENAME_ALL: Symbol = Symbol("rename_all");
pub const RENAME_ALL_FIELDS: Symbol = Symbol("rename_all_fields");

ripgrep syntax

greppy recognizes ripgrep-style invocations by the name rg or by flags that belong to ripgrep and that grep lacks, such as --smart-case, -t rust, -g '!target' or --json:

greppy rg -S deny_unknown_fields serde_derive/src
greppy --smart-case -t rust -n deny_unknown_fields serde_derive
greppy rg -g '!test_suite' -n 'fn deserialize_struct' .
$ greppy rg -n --sort path deny_unknown_fields serde_derive/src
serde_derive/src/de/enum_adjacently.rs:52:    let deny_unknown_fields = cattrs.deny_unknown_fields();
serde_derive/src/de/enum_adjacently.rs:56:    let field_visitor_ty = if deny_unknown_fields {
serde_derive/src/de/enum_adjacently.rs:122:    let next_relevant_key = if deny_unknown_fields {
…

greppy routes them in this order:

  1. When a real rg is installed, greppy delegates to it byte for byte.
  2. Otherwise greppy maps the common flags onto the grep passthrough.
  3. Anything grep cannot express fails loudly, with the closest alternative. It never silently turns into a different search.

ripgrep searches files in parallel, so the order of its output can change from run to run. For comparisons, you set --sort path.

Commands and patterns

greppy tells a command from a pattern by its shape. A mistyped command has no file operand, and that is what separates a typo from a pattern:

$ greppy renames serde_derive/src/internals/attr.rs
                    let (ser, de) = get_renames(cx, RENAME, &meta)?;
                    let (ser, de) = get_renames(cx, RENAME_ALL, &meta)?;
…
$ greppy who-call visit_content_map
status: invalid_invocation
command: `who-call`
message: unknown greppy command; nothing was passed to grep
next: did you mean `greppy who-calls ...`?

greppy renames FILE stays grep and prints the matching lines, like the real grep. greppy who-call visit_content_map ends with exit code 64 and a did you mean hint.

A leading -p is reserved for the built-in agent. To search for the literal pattern -p, you use -e:

$ greppy -e -p .github/workflows/ci.yml
      - run: cargo docs-rs -p serde
      - run: cargo docs-rs -p serde_core
…

greppy's own flags are not grep flags. If a grep invocation carries a flag that belongs to greppy, such as --code, greppy refuses it with exit code 64, says where the flag belongs and passes nothing to grep:

$ greppy -rn deny_unknown_fields serde_derive/src --code
`--code` is a greppy flag, not a grep flag - it belongs to greppy's navigation commands. Drop it, or name the command it goes with.

After an explicit rg, every following option belongs to ripgrep, so greppy rg --json … reaches ripgrep unchanged.

Standard input

Pipelines work as they do with grep:

$ ls serde_derive/src/de | greppy -n enum
1:enum_.rs
2:enum_adjacently.rs
3:enum_externally.rs
4:enum_internally.rs
5:enum_untagged.rs

An explicit - operand is the unbounded, byte-exact path for slow producers. When the sole operand is a word that looks like a mistyped greppy command, such as brif, greppy gives an implicit stdin five seconds to start. If nothing arrives, it reports the typo instead of waiting, exit code 64:

$ greppy brif
status: invalid_invocation
command: `brif`
message: unknown greppy command; nothing was passed to grep
next: did you mean `greppy brief ...`?

Other patterns wait for their producer, as grep does.

Not a grep replacement

greppy installs as greppy, never as a global grep alias and never under the name grep. Your system grep stays your grep.

Inside greppy, the passthrough is the tool for string-level certainty: when you need every textual occurrence of a name, including in comments, strings and files the graph does not model, grep is the right answer. For text matches that also name the enclosing definition, you use search-pattern.

For agents

The agent prompt tells agents to stay inside greppy for text search too: greppy PATTERN [FILE] is byte-identical grep and greppy rg … is ripgrep, so every search goes through one tool. See Agent setup.