brep -> gg
This commit is contained in:
@@ -1,20 +1,21 @@
|
|||||||
# brep - minimal grep-like utility built on POSIX mmap() + SIMD + threads
|
# gg - Grokkin' Grep: a minimal, fast, portable grep-like file finder.
|
||||||
#
|
#
|
||||||
# Build: make
|
# Build: make
|
||||||
# Clean: make clean
|
# Clean: make clean
|
||||||
# Install: make install PREFIX=/usr/local
|
# Install: make install PREFIX=/usr/local
|
||||||
|
|
||||||
CC ?= gcc
|
CC ?= gcc
|
||||||
CFLAGS ?= -O2 -std=c11 -Wall -Wextra
|
CFLAGS ?= -O2 -s -std=c11 -Wall -Wextra
|
||||||
CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
|
CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
|
||||||
LDFLAGS ?=
|
LDFLAGS ?=
|
||||||
LDLIBS += -lpthread
|
LDLIBS += -lpthread
|
||||||
# Enable AVX2 for the SIMD matcher (needs a 2008+ Intel/AMD CPU).
|
# The matcher uses POSIX memmem(), which glibc accelerates with SIMD on
|
||||||
# Drop -mavx2 if targeting older hardware; the matcher still compiles.
|
# whatever CPU you run on. -mavx2 is harmless but unnecessary; drop it for
|
||||||
|
# maximum portability (e.g. make MARCH="").
|
||||||
MARCH ?= -mavx2
|
MARCH ?= -mavx2
|
||||||
|
|
||||||
BINARY := brep
|
BINARY := gg
|
||||||
SRC := brep.c
|
SRC := gg.c
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
BINDIR := $(PREFIX)/bin
|
BINDIR := $(PREFIX)/bin
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# 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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## 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.** Unlike `rg`'s default, `gg`
|
||||||
|
does not consult `.gitignore` or skip hidden 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`?
|
||||||
|
|
||||||
|
No. `gg` searches everything it can read, including hidden files and directories.
|
||||||
|
This is why its 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.
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
/*
|
|
||||||
* brep - minimal grep-like utility for basic text search.
|
|
||||||
*
|
|
||||||
* Usage: brep SEARCH_TERM [FILE_OR_DIRECTORY]
|
|
||||||
*
|
|
||||||
* Recursively walks FILE_OR_DIRECTORY (default: current working directory),
|
|
||||||
* reads every regular file, searches it for the case-sensitive SEARCH_TERM,
|
|
||||||
* and prints the paths of all files containing at least one occurrence
|
|
||||||
* (one per line).
|
|
||||||
*
|
|
||||||
* Implementation notes
|
|
||||||
* --------------------
|
|
||||||
* Scanning is split into two phases so that the expensive work parallelises
|
|
||||||
* well:
|
|
||||||
*
|
|
||||||
* 1. Walk: a single thread enumerates every regular file in the tree into a
|
|
||||||
* growable array of paths. The walk uses d_type from readdir and only
|
|
||||||
* falls back to lstat for DT_UNKNOWN entries; symlinks are not followed
|
|
||||||
* (cycle-safe). This phase is cheap (it touches metadata, not file data).
|
|
||||||
*
|
|
||||||
* 2. Scan: N worker threads grab files from a single atomic counter and scan
|
|
||||||
* each one independently. Files are read with read() into a small
|
|
||||||
* per-thread buffer (avoids the mmap/munmap setup and the page-fault
|
|
||||||
* storm mmap incurs across a large tree, while keeping RAM bounded to one
|
|
||||||
* buffer per worker). Chunks overlap by needle_len-1 bytes so a match
|
|
||||||
* straddling a chunk boundary is still found.
|
|
||||||
*
|
|
||||||
* Binary files are skipped, matching ripgrep's default behaviour: a file that
|
|
||||||
* contains a NUL byte anywhere is treated as binary and is never reported,
|
|
||||||
* even if it contains the search term. Detection is cheap: we search for the
|
|
||||||
* needle with memmem (a single pass over the data); only when a needle is
|
|
||||||
* found do we probe the already-read chunk for a NUL byte, so the common
|
|
||||||
* non-matching path pays no extra scan.
|
|
||||||
*
|
|
||||||
* Matching uses POSIX memmem(); the scan phase runs across the online CPUs.
|
|
||||||
*
|
|
||||||
* Exit codes: 0 - at least one file matched,
|
|
||||||
* 1 - no matches,
|
|
||||||
* 2 - usage or input error.
|
|
||||||
*
|
|
||||||
* No third-party dependencies, no external commands.
|
|
||||||
*/
|
|
||||||
#ifndef _POSIX_C_SOURCE
|
|
||||||
#define _POSIX_C_SOURCE 200809L
|
|
||||||
#endif
|
|
||||||
#ifndef _DEFAULT_SOURCE
|
|
||||||
#define _DEFAULT_SOURCE
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#ifndef PATH_MAX
|
|
||||||
#define PATH_MAX 4096
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define IO_BUFSZ (32u * 1024) /* per-thread read buffer */
|
|
||||||
|
|
||||||
/* ---------- shared scan state ---------- */
|
|
||||||
static const char *needle;
|
|
||||||
static size_t needle_len;
|
|
||||||
static int found_any = 0;
|
|
||||||
static pthread_mutex_t found_mx = PTHREAD_MUTEX_INITIALIZER;
|
|
||||||
|
|
||||||
/* ---------- collected file paths ---------- */
|
|
||||||
static char **paths = NULL;
|
|
||||||
static size_t npaths = 0, path_cap = 0;
|
|
||||||
static pthread_mutex_t paths_mx = PTHREAD_MUTEX_INITIALIZER;
|
|
||||||
static size_t scan_idx = 0; /* atomic work counter */
|
|
||||||
|
|
||||||
static void die(const char *msg) {
|
|
||||||
perror(msg);
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void add_path(const char *p) {
|
|
||||||
pthread_mutex_lock(&paths_mx);
|
|
||||||
if (npaths == path_cap) {
|
|
||||||
path_cap = path_cap ? path_cap * 2 : 1 << 16;
|
|
||||||
paths = realloc(paths, path_cap * sizeof *paths);
|
|
||||||
if (paths == NULL)
|
|
||||||
die("realloc");
|
|
||||||
}
|
|
||||||
paths[npaths++] = strdup(p);
|
|
||||||
if (paths[npaths - 1] == NULL)
|
|
||||||
die("strdup");
|
|
||||||
pthread_mutex_unlock(&paths_mx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- walk: collect regular files ---------- */
|
|
||||||
static void walk(const char *dir) {
|
|
||||||
DIR *d = opendir(dir);
|
|
||||||
if (d == NULL)
|
|
||||||
return;
|
|
||||||
struct dirent *ent;
|
|
||||||
char child[PATH_MAX];
|
|
||||||
while ((ent = readdir(d)) != NULL) {
|
|
||||||
if (strcmp(ent->d_name, ".") == 0 ||
|
|
||||||
strcmp(ent->d_name, "..") == 0)
|
|
||||||
continue;
|
|
||||||
int n = snprintf(child, sizeof child, "%s/%s", dir, ent->d_name);
|
|
||||||
if (n < 0 || (size_t)n >= sizeof child)
|
|
||||||
continue; /* path too long */
|
|
||||||
|
|
||||||
if (ent->d_type == DT_REG) {
|
|
||||||
add_path(child);
|
|
||||||
} else if (ent->d_type == DT_DIR) {
|
|
||||||
walk(child);
|
|
||||||
} else if (ent->d_type == DT_LNK) {
|
|
||||||
continue; /* do not follow symlinks: avoids cycles */
|
|
||||||
} else {
|
|
||||||
struct stat st;
|
|
||||||
if (lstat(child, &st) != 0)
|
|
||||||
continue;
|
|
||||||
if (S_ISDIR(st.st_mode))
|
|
||||||
walk(child);
|
|
||||||
else if (S_ISREG(st.st_mode))
|
|
||||||
add_path(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closedir(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- scan one open fd (chunked read + memmem, binary-skipping) ---------- */
|
|
||||||
typedef struct {
|
|
||||||
char *buf;
|
|
||||||
char *carry;
|
|
||||||
} ScanBufs;
|
|
||||||
|
|
||||||
static int scan_fd(int fd, ScanBufs *sb) {
|
|
||||||
char *buf = sb->buf;
|
|
||||||
char *carry = sb->carry;
|
|
||||||
size_t carryn = 0;
|
|
||||||
int hit = 0;
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
ssize_t r = read(fd, buf + carryn, IO_BUFSZ);
|
|
||||||
if (r < 0) {
|
|
||||||
if (errno == EINTR)
|
|
||||||
continue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (r == 0)
|
|
||||||
break; /* EOF */
|
|
||||||
size_t wn = carryn + (size_t)r;
|
|
||||||
|
|
||||||
if (memmem(buf, wn, needle, needle_len) != NULL) {
|
|
||||||
/* Candidate match. A NUL byte anywhere in the data read marks a binary
|
|
||||||
* file, which we skip (ripgrep's default). The probe is bounded to the
|
|
||||||
* chunk already in memory, so it is cheap and only runs on the rare
|
|
||||||
* files that actually contain the needle. */
|
|
||||||
if (memchr(buf, '\0', wn) == NULL) {
|
|
||||||
hit = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break; /* binary file that happens to contain the needle */
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wn >= needle_len)
|
|
||||||
carryn = needle_len - 1;
|
|
||||||
else
|
|
||||||
carryn = wn;
|
|
||||||
memcpy(carry, buf + wn - carryn, carryn);
|
|
||||||
memcpy(buf, carry, carryn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return hit;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void report_match(const char *path) {
|
|
||||||
pthread_mutex_lock(&found_mx);
|
|
||||||
found_any = 1;
|
|
||||||
puts(path);
|
|
||||||
pthread_mutex_unlock(&found_mx);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void scan_file(const char *path, ScanBufs *sb) {
|
|
||||||
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
||||||
if (fd < 0)
|
|
||||||
return; /* unreadable entries are silently skipped */
|
|
||||||
if (scan_fd(fd, sb))
|
|
||||||
report_match(path);
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- worker: grab files by atomic index, scan ---------- */
|
|
||||||
static void *worker(void *arg) {
|
|
||||||
(void)arg;
|
|
||||||
ScanBufs sb;
|
|
||||||
sb.buf = malloc(IO_BUFSZ + needle_len);
|
|
||||||
sb.carry = malloc(needle_len);
|
|
||||||
if (sb.buf == NULL || sb.carry == NULL) {
|
|
||||||
free(sb.buf);
|
|
||||||
free(sb.carry);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
for (;;) {
|
|
||||||
size_t i = __sync_fetch_and_add(&scan_idx, 1);
|
|
||||||
if (i >= npaths)
|
|
||||||
break;
|
|
||||||
scan_file(paths[i], &sb);
|
|
||||||
}
|
|
||||||
free(sb.buf);
|
|
||||||
free(sb.carry);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
|
||||||
if (argc < 2 || argc > 3) {
|
|
||||||
fprintf(stderr, "usage: %s SEARCH_TERM [FILE_OR_DIRECTORY]\n", argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
needle = argv[1];
|
|
||||||
needle_len = strlen(needle);
|
|
||||||
if (needle_len == 0) {
|
|
||||||
fprintf(stderr, "brep: empty search term\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
char cwd[PATH_MAX];
|
|
||||||
const char *root;
|
|
||||||
if (argc == 3) {
|
|
||||||
struct stat st;
|
|
||||||
if (stat(argv[2], &st) != 0) {
|
|
||||||
fprintf(stderr, "brep: %s: %s\n", argv[2], strerror(errno));
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
|
|
||||||
fprintf(stderr, "brep: %s: not a regular file or directory\n", argv[2]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
root = argv[2];
|
|
||||||
} else {
|
|
||||||
if (getcwd(cwd, sizeof cwd) == NULL)
|
|
||||||
die("getcwd");
|
|
||||||
root = cwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct stat st;
|
|
||||||
if (stat(root, &st) != 0) {
|
|
||||||
fprintf(stderr, "brep: %s: %s\n", root, strerror(errno));
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Phase 1: collect files (cheap metadata walk). */
|
|
||||||
if (S_ISREG(st.st_mode)) {
|
|
||||||
add_path(root);
|
|
||||||
} else {
|
|
||||||
walk(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (npaths == 0)
|
|
||||||
return 1; /* nothing to scan */
|
|
||||||
|
|
||||||
/* Phase 2: parallel scan. */
|
|
||||||
long nproc = sysconf(_SC_NPROCESSORS_ONLN);
|
|
||||||
if (nproc < 1)
|
|
||||||
nproc = 1;
|
|
||||||
int nth = (int)nproc;
|
|
||||||
if (nth > 256)
|
|
||||||
nth = 256;
|
|
||||||
|
|
||||||
pthread_t t[256];
|
|
||||||
int started = 0;
|
|
||||||
for (int i = 0; i < nth; i++) {
|
|
||||||
if (pthread_create(&t[i], NULL, worker, NULL) != 0)
|
|
||||||
break;
|
|
||||||
started++;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < started; i++)
|
|
||||||
pthread_join(t[i], NULL);
|
|
||||||
|
|
||||||
return found_any ? 0 : 1;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* gg - Grokkin' Grep: a minimal, fast, portable grep-like file finder
|
||||||
|
*
|
||||||
|
* Usage: gg SEARCH_TERM [FILE_OR_DIRECTORY]
|
||||||
|
*
|
||||||
|
* Created by Luxferre in 2026, released into the public domain
|
||||||
|
*/
|
||||||
|
#ifndef _POSIX_C_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
#endif
|
||||||
|
#ifndef _DEFAULT_SOURCE
|
||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifndef PATH_MAX
|
||||||
|
#define PATH_MAX 4096
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define IO_BUFSZ (32u * 1024) /* per-thread read buffer */
|
||||||
|
|
||||||
|
/* ---------- shared scan state ---------- */
|
||||||
|
static const char *needle;
|
||||||
|
static size_t needle_len;
|
||||||
|
static int found_any = 0;
|
||||||
|
static pthread_mutex_t found_mx = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
/* ---------- directory work queue (parallel walk + scan) ---------- */
|
||||||
|
typedef struct DirJob {
|
||||||
|
char *path;
|
||||||
|
struct DirJob *next;
|
||||||
|
} DirJob;
|
||||||
|
|
||||||
|
static DirJob *dir_stack = NULL;
|
||||||
|
static pthread_mutex_t dir_mx = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
static void push_dir(const char *path) {
|
||||||
|
DirJob *j = malloc(sizeof *j);
|
||||||
|
if (j == NULL)
|
||||||
|
return;
|
||||||
|
j->path = strdup(path);
|
||||||
|
if (j->path == NULL) {
|
||||||
|
free(j);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&dir_mx);
|
||||||
|
j->next = dir_stack;
|
||||||
|
dir_stack = j;
|
||||||
|
pthread_mutex_unlock(&dir_mx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void die(const char *msg) {
|
||||||
|
perror(msg);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *pop_dir(void) {
|
||||||
|
pthread_mutex_lock(&dir_mx);
|
||||||
|
DirJob *j = dir_stack;
|
||||||
|
if (j != NULL)
|
||||||
|
dir_stack = j->next;
|
||||||
|
pthread_mutex_unlock(&dir_mx);
|
||||||
|
if (j == NULL)
|
||||||
|
return NULL;
|
||||||
|
char *p = j->path;
|
||||||
|
free(j);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- scan one open fd (chunked read + memmem, binary-skipping) ---------- */
|
||||||
|
typedef struct {
|
||||||
|
char *buf;
|
||||||
|
char *carry;
|
||||||
|
} ScanBufs;
|
||||||
|
|
||||||
|
static int scan_fd(int fd, ScanBufs *sb) {
|
||||||
|
char *buf = sb->buf;
|
||||||
|
char *carry = sb->carry;
|
||||||
|
size_t carryn = 0;
|
||||||
|
int hit = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
ssize_t r = read(fd, buf + carryn, IO_BUFSZ);
|
||||||
|
if (r < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (r == 0)
|
||||||
|
break; /* EOF */
|
||||||
|
size_t wn = carryn + (size_t)r;
|
||||||
|
|
||||||
|
/* Binary detection: a NUL byte marks a binary file, which we skip (like
|
||||||
|
* ripgrep). Stop reading the moment we see one so we do not waste
|
||||||
|
* bandwidth scanning the rest of a large binary file. If a needle occurs
|
||||||
|
* before the first NUL it is still a valid text match. */
|
||||||
|
void *nul = memchr(buf, '\0', wn);
|
||||||
|
size_t text_end = (nul != NULL) ? (size_t)((const char *)nul - buf) : wn;
|
||||||
|
if (text_end >= needle_len) {
|
||||||
|
if (memmem(buf, text_end, needle, needle_len) != NULL) {
|
||||||
|
hit = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nul != NULL)
|
||||||
|
break; /* binary file: no further scanning */
|
||||||
|
|
||||||
|
/* No NUL and no match yet: carry the tail across chunks. */
|
||||||
|
if (wn >= needle_len)
|
||||||
|
carryn = needle_len - 1;
|
||||||
|
else
|
||||||
|
carryn = wn;
|
||||||
|
memcpy(carry, buf + wn - carryn, carryn);
|
||||||
|
memcpy(buf, carry, carryn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void report_match(const char *path) {
|
||||||
|
pthread_mutex_lock(&found_mx);
|
||||||
|
found_any = 1;
|
||||||
|
puts(path);
|
||||||
|
pthread_mutex_unlock(&found_mx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void scan_file(const char *path, ScanBufs *sb) {
|
||||||
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||||
|
if (fd < 0)
|
||||||
|
return; /* unreadable entries are silently skipped */
|
||||||
|
if (scan_fd(fd, sb))
|
||||||
|
report_match(path);
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- worker: pop a directory, scan its files, enqueue subdirs ---------- */
|
||||||
|
static void *worker(void *arg) {
|
||||||
|
(void)arg;
|
||||||
|
ScanBufs sb;
|
||||||
|
sb.buf = malloc(IO_BUFSZ + needle_len);
|
||||||
|
sb.carry = malloc(needle_len);
|
||||||
|
if (sb.buf == NULL || sb.carry == NULL) {
|
||||||
|
free(sb.buf);
|
||||||
|
free(sb.carry);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
char child[PATH_MAX];
|
||||||
|
for (;;) {
|
||||||
|
char *dir = pop_dir();
|
||||||
|
if (dir == NULL)
|
||||||
|
break;
|
||||||
|
DIR *d = opendir(dir);
|
||||||
|
if (d != NULL) {
|
||||||
|
struct dirent *ent;
|
||||||
|
while ((ent = readdir(d)) != NULL) {
|
||||||
|
if (strcmp(ent->d_name, ".") == 0 ||
|
||||||
|
strcmp(ent->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
int n = snprintf(child, sizeof child, "%s/%s", dir, ent->d_name);
|
||||||
|
if (n < 0 || (size_t)n >= sizeof child)
|
||||||
|
continue;
|
||||||
|
if (ent->d_type == DT_REG) {
|
||||||
|
scan_file(child, &sb);
|
||||||
|
} else if (ent->d_type == DT_DIR) {
|
||||||
|
push_dir(child);
|
||||||
|
} else if (ent->d_type == DT_LNK) {
|
||||||
|
continue; /* do not follow symlinks: avoids cycles */
|
||||||
|
} else {
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(child, &st) != 0)
|
||||||
|
continue;
|
||||||
|
if (S_ISDIR(st.st_mode))
|
||||||
|
push_dir(child);
|
||||||
|
else if (S_ISREG(st.st_mode))
|
||||||
|
scan_file(child, &sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
}
|
||||||
|
free(dir);
|
||||||
|
}
|
||||||
|
free(sb.buf);
|
||||||
|
free(sb.carry);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2 || argc > 3) {
|
||||||
|
fprintf(stderr, "usage: %s SEARCH_TERM [FILE_OR_DIRECTORY]\n", argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
needle = argv[1];
|
||||||
|
needle_len = strlen(needle);
|
||||||
|
if (needle_len == 0) {
|
||||||
|
fprintf(stderr, "gg: empty search term\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
char cwd[PATH_MAX];
|
||||||
|
const char *root;
|
||||||
|
if (argc == 3) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(argv[2], &st) != 0) {
|
||||||
|
fprintf(stderr, "gg: %s: %s\n", argv[2], strerror(errno));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
|
||||||
|
fprintf(stderr, "gg: %s: not a regular file or directory\n", argv[2]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
root = argv[2];
|
||||||
|
} else {
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL)
|
||||||
|
die("getcwd");
|
||||||
|
root = cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(root, &st) != 0) {
|
||||||
|
fprintf(stderr, "gg: %s: %s\n", root, strerror(errno));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Seed the directory work queue, then let the workers walk and scan in
|
||||||
|
* parallel: each worker pops a directory, scans its regular files inline,
|
||||||
|
* and pushes any sub-directories it finds. This overlaps the (metadata)
|
||||||
|
* walk with the (data) scan across all online CPUs. */
|
||||||
|
if (S_ISREG(st.st_mode)) {
|
||||||
|
scan_file(root, &(ScanBufs){ malloc(IO_BUFSZ + needle_len),
|
||||||
|
malloc(needle_len) });
|
||||||
|
} else {
|
||||||
|
push_dir(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
long nproc = sysconf(_SC_NPROCESSORS_ONLN);
|
||||||
|
if (nproc < 1)
|
||||||
|
nproc = 1;
|
||||||
|
int nth = (int)nproc;
|
||||||
|
if (nth > 256)
|
||||||
|
nth = 256;
|
||||||
|
|
||||||
|
pthread_t t[256];
|
||||||
|
int started = 0;
|
||||||
|
for (int i = 0; i < nth; i++) {
|
||||||
|
if (pthread_create(&t[i], NULL, worker, NULL) != 0)
|
||||||
|
break;
|
||||||
|
started++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < started; i++)
|
||||||
|
pthread_join(t[i], NULL);
|
||||||
|
|
||||||
|
return found_any ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user