Features

Everything your agent knows keeps working

What you get

Your agent works with grep and ripgrep. With greppy it keeps both: greppy -rn PATTERN . is grep, greppy rg … is ripgrep, and output and exit code are the same. On top of that it gets greppy's commands for graph, search, reading and editing. One tool for everything, and no familiar call breaks.

Before and after

The question for serde: Where are the traits that start with pub trait Deserialize?

With grep

$ grep -rn "pub trait Deserialize" --include=*.rs .
./serde_core/src/de/mod.rs:554:pub trait Deserialize<'de>: Sized {
./serde_core/src/de/mod.rs:632:pub trait DeserializeOwned: for<'de> Deserialize<'de> {}
./serde_core/src/de/mod.rs:803:pub trait DeserializeSeed<'de>: Sized {
./serde_core/src/de/mod.rs:945:pub trait Deserializer<'de>: Sized {
./test_suite/tests/test_serde_path.rs:38:    pub trait Deserialize<'a>: Sized {

With greppy

$ greppy -rn "pub trait Deserialize" --include=*.rs .
./serde_core/src/de/mod.rs:554:pub trait Deserialize<'de>: Sized {
./serde_core/src/de/mod.rs:632:pub trait DeserializeOwned: for<'de> Deserialize<'de> {}
./serde_core/src/de/mod.rs:803:pub trait DeserializeSeed<'de>: Sized {
./serde_core/src/de/mod.rs:945:pub trait Deserializer<'de>: Sized {
./test_suite/tests/test_serde_path.rs:38:    pub trait Deserialize<'a>: Sized {

Byte for byte

cmp compares both outputs, for grep and for ripgrep.

$ grep -rn "pub trait Deserialize" --include=*.rs . > grep.out; echo "exit $?"
exit 0
$ greppy -rn "pub trait Deserialize" --include=*.rs . > greppy.out; echo "exit $?"
exit 0
$ cmp grep.out greppy.out && echo identical
identical
$ rg -n -t rust --sort path "pub trait Deserialize" . > rg.out; echo "exit $?"
exit 0
$ greppy rg -n -t rust --sort path "pub trait Deserialize" . > greppy-rg.out; echo "exit $?"
exit 0
$ cmp rg.out greppy-rg.out && echo identical
identical

The same holds for error cases, pipes and ripgrep flags. Every case ran once directly and once through greppy, with stdout and stderr compared separately:

Case Call through greppy Exit direct / through greppy stdout and stderr
hit, 374 bytes greppy -rn "pub trait Deserialize" --include=*.rs . 0 / 0 identical
no hit greppy -rn "no_such_symbol_xyz" --include=*.rs . 1 / 1 identical
file missing greppy -n "Deserialize" does/not/exist.rs 2 / 2 identical, including the message grep: does/not/exist.rs: No such file or directory
pipe on stdin cat serde_core/src/de/mod.rs | greppy -c "fn deserialize" 0 / 0 identical (38)
leading grep word greppy grep -rlw visit_content_map serde 0 / 0 identical
ripgrep greppy rg -n -t rust --sort path "pub trait Deserialize" 0 / 0 identical
ripgrep flags without the rg word greppy --smart-case -t rust --sort path "pub trait deserialize" 0 / 0 identical

Where greppy steps in

greppy passes real grep calls through. It recognizes two cases beforehand and says what is meant, instead of letting grep search for the wrong thing.

$ greppy -rn "visit_content_map" --code serde
`--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.
$ 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 ...`?

Both end with exit 64. A leading -p starts the built-in agent; for the pattern -p you write greppy -e -p FILE, with the same output as grep -e -p FILE.

How it works

greppy tells from the shape of a call whether one of its commands is meant. Everything else starts the system grep with the same arguments, and stdout, stderr and exit code come back unchanged. The passthrough calls the system grep directly and needs no index: in a directory without an index, greppy -rn TODO . creates neither a store nor a file in greppy's data directory.

greppy recognizes ripgrep calls by the word rg or by flags that exist in ripgrep alone, such as --smart-case or -t rust. It hands them to your installed rg. ripgrep searches files in parallel; with --sort path the order is the same on every run.

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

The commands

Command what it does
greppy PATTERN [FILE], greppy -rn PATTERN PATH real grep, output and exit code byte-identical
greppy grep … the same with a leading grep word
greppy rg … ripgrep through your installed rg, byte-identical
greppy --smart-case -t rust PATTERN ripgrep flags are recognized and routed to rg
greppy -e -p FILE grep for the pattern -p
greppy search-pattern REGEX text search that names the enclosing definition for every hit

More in the docs