Files
gg/README.md
T
2026-08-21 13:56:57 +03:00

261 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Grokkin' Grep (`gg`)
GG is a minimal, fast, and **portable** grep-like utility that finds files
containing a literal substring. `gg` prints the paths of every file that contains
the search term — one path per line — and is designed to be competitive with
The Silver Searcher and ripgrep on large source trees while depending on nothing
but POSIX and the C standard library.
```
$ gg "public domain" ~/proj
/home/user/proj/foo/license.txt
/home/user/proj/bar/COPYING
...
```
## About
GG answers one question well: *which files contain this exact text?*
It is intentionally narrow:
- **Literal, case-sensitive substring search.** No regex, no flags, no
surprises. The term you pass is matched byte-for-byte.
- **File-finding, not line-finding.** Output is a list of matching file paths,
not the matching lines. (Pipe to `grep -n` if you want line numbers.)
- **Binary-aware.** Files containing a NUL byte are treated as binary and
skipped. A literal match that occurs *before* the first NUL in an
otherwise-text file is still reported.
- **Recursive by default.** Given a directory it walks the whole tree.
- **Git-aware by default.** When walking a tree, `gg` honours every `.gitignore`
it encounters and always skips `.git` directories — just like `git` and `rg`.
Set `GG_GIT_ALL=1` to search everything instead (equivalent to `rg
--no-ignore`).
- **Self-contained.** No third-party libraries, no shelling out to other
programs. Just `libc` + `libpthread`.
`gg` was built to be a clean, readable reference implementation that is also
fast: on a 16 GB / ~209 000-file tree it completes the equivalent of
`rg --no-ignore --hidden -l` in roughly **0.4 s**, about **1.5× faster than
`rg`** on the same machine and query, using only portable POSIX interfaces. (With `GG_GIT_ALL` unset, the equivalent is `rg --hidden -l`.)
## Installation
### From source
You need a C compiler (`gcc` or `clang`) and `make`. No other dependencies.
```sh
git clone <repo-url> gg && cd gg
make
# optional, system-wide:
sudo make install PREFIX=/usr/local
```
This produces a single executable named `gg`.
### Build options
- **Portable build (no x86-specific flags):**
```sh
make MARCH=""
```
`gg` uses POSIX `memmem()` for matching; glibc (and other libcs) already
accelerate that with SIMD on whatever CPU you run on. The default `-mavx2`
flag is therefore optional and can be dropped for maximum portability.
- **Cross-compiling for ARM / other targets:**
```sh
aarch64-linux-gnu-gcc -O2 -std=c11 -Wall -Wextra \
-D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -o gg gg.c -lpthread
```
`gg` contains no SIMD intrinsics and no platform-specific code, so it builds
and runs unchanged on ARM64 and other POSIX platforms.
### Uninstall
```sh
sudo make uninstall PREFIX=/usr/local
```
## Usage
```
gg SEARCH_TERM [FILE_OR_DIRECTORY]
```
- `SEARCH_TERM` — required. A case-sensitive literal substring to find. Must be
non-empty.
- `FILE_OR_DIRECTORY` — optional. Defaults to the current working directory.
May be a single regular file or a directory (which is walked recursively).
### Examples
```sh
# Find every file under the current directory containing "TODO"
gg TODO
# Search a specific tree for a license header
gg "public domain" ~/proj
# Check a single file
gg "def main" src/main.py
```
### Exit codes
| Code | Meaning |
|------|---------|
| `0` | At least one file matched. |
| `1` | No files matched. |
| `2` | Usage or input error (missing/empty term, bad path, etc.). |
### Behaviour notes
- **Symlinks are not followed.** Symbolic links to files or directories are
skipped, which prevents infinite loops on symlink cycles.
- **Hidden files and directories are searched.** `gg` does not skip hidden
paths. It *does* consult `.gitignore` files and skips `.git` directories by
default; set `GG_GIT_ALL=1` to disable that and search everything (including
ignored and `.git` paths).
- **Binary files are skipped.** A file containing a NUL byte is never reported,
even if it also contains the search term (except when the match occurs before
the first NUL, in which case it is reported — see FAQ).
## How it works
`gg` is a small, readable program. The full algorithm, in high-level terms:
1. **Parse arguments.** Read the search term (must be non-empty) and the optional
target (a file or directory, defaulting to the current directory).
2. **Parallel walk + scan (the core idea).** Rather than walking the whole tree
first and *then* scanning, `gg` overlaps the two. A shared, mutex-protected
**directory work-queue** holds directories still to be processed. `N` worker
threads (one per online CPU) each:
a. **Pop a directory** from the queue.
b. **Open it** with `opendir`/`readdir` and iterate its entries.
c. For each entry, use `d_type` from `readdir` to classify it cheaply:
- `DT_REG` (regular file) → scan it immediately (see step 3).
- `DT_DIR` (sub-directory) → **push it onto the work-queue** for another
worker to pick up.
- `DT_LNK` (symlink) → skip it (no following, no cycles).
- `DT_UNKNOWN` → fall back to `lstat` to decide file vs. directory, then
act accordingly.
d. **Free the directory** and loop back to pop the next one.
Because directories are handed out one at a time and sub-directories are
re-enqueued, the metadata walk and the data scan proceed concurrently across
all cores, and no single deep directory becomes a bottleneck for any one
thread.
3. **Scan a file (early binary detection + streaming match).** For each regular
file the worker:
a. Opens it with `open(O_RDONLY | O_CLOEXEC)`.
b. Reads it in **fixed-size chunks** (32 KB) into a small per-thread buffer.
Chunks intentionally **overlap by `len(term) - 1` bytes**, so a match that
straddles a chunk boundary is never missed.
c. On each chunk, scans for a NUL byte with `memchr`:
- If a NUL is found, the file is **binary**: `gg` stops reading
immediately (no need to scan the rest) and reports nothing for it.
- Otherwise it searches the text portion of the chunk for the term with
`memmem`. If found, the file is a match and scanning stops.
d. If the term is not in this chunk and no NUL was seen, it carries the
overlap into the next read and continues.
Stopping at the first NUL is what makes `gg` fast on trees full of binary
artifacts (archives, object files, images): those files are abandoned after a
single tiny read instead of being fully scanned.
4. **Report matches.** The first time any worker finds a match, it records a
global "found" flag and prints the file path. Reporting is guarded by a mutex
so output stays coherent; because matching files are comparatively rare, this
lock is almost never contended.
5. **Finish.** When the work-queue is empty and all workers have exited, `gg`
returns `0` if anything matched, otherwise `1`.
In short: **per-directory work units + inline file scanning + stop-at-NUL binary
skipping + POSIX `memmem` matching**, all parallelised across the online CPUs.
No `mmap`, no regex engine, no external processes — just portable syscalls and
library calls.
## FAQ
### Why is it called Grokkin' Grep?
A playful name for a tool that "groks" your files to find text. `gg` is also
nicely short to type.
### Is the search case-sensitive?
Yes. `gg` matches the term exactly as given. There is no case-insensitive mode.
### Does it support regular expressions?
No. `gg` is a literal substring finder. For regex, use `grep`, `rg`, or `ag`.
### Why does it print file paths instead of matching lines?
`gg` is a *file finder* — it answers "which files contain this?". Pipe its output
through `xargs grep -n` (or `rg`) if you need the lines and line numbers.
### How is a "binary file" decided, and why skip it?
A file is considered binary if it contains a NUL (`\0`) byte. This matches
`ripgrep`'s default heuristic. Such files are skipped because text search over
them is usually meaningless. If your term appears *before* the first NUL in an
otherwise-readable file, `gg` still reports it (it only abandons the file once it
reaches the NUL).
### Does it follow symlinks?
No. Symbolic links to files or directories are skipped, which makes `gg` safe
against symlink cycles and avoids double-counting.
### Does it respect `.gitignore`?
Yes, by default. `gg` reads every `.gitignore` found while walking and skips
matching files and directories, and it always skips `.git` directories — the
same rules `git` itself uses (including `!` negation, `**` globs, and
trailing-`/` directory-only patterns). Hidden files and directories are still
searched; only `.gitignore`-matched paths and `.git` are excluded.
To search everything — ignored files, `.git`, the lot — set the `GG_GIT_ALL`
environment variable (to any value) before running `gg`:
```sh
GG_GIT_ALL=1 gg "needle" .
```
With `GG_GIT_ALL` set, `gg`'s results match `rg --no-ignore --hidden`.
### Why not `mmap` the files?
`mmap` was prototyped and rejected: across hundreds of thousands of files the
`munmap` syscall storm dominated, and `read()` into a small per-thread buffer
scaled better. `read()` also avoids faulting the entire file into memory.
### Is it really portable / dependency-free?
Yes. The only external links are `libc` and `libpthread`. There are no SIMD
intrinsics in the source; matching uses `memmem()`, which your libc already
accelerates with SIMD on the host CPU. It builds and runs on x86-64 and ARM64
alike.
### How fast is it?
On a warm cache over a 16 GB / ~209 000-file tree, `gg "public domain" ~/proj`
completes in roughly **0.4 s** — about **1.5× faster** than the equivalent
`rg --no-ignore --hidden -l` on the same hardware, using only portable POSIX
interfaces.
## Credits
Created by Luxferre in 2026, released into the public domain with no warranties.