/* * 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 #include #include #include #include #include #include #include #include #include #include #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; static int git_all = 0; /* if set, ignore .gitignore / .git entirely */ /* ---------- .gitignore handling ---------- */ typedef struct Pattern { int negate; /* pattern began with '!' */ int dir_only; /* pattern ended with '/' (matches dirs only) */ int has_slash; /* pattern contains a '/' (anchored to its .gitignore dir) */ char *pat; /* pattern text (without '!' / trailing '/') */ size_t pat_len; } Pattern; typedef struct GitIgnore { Pattern *pats; size_t npats; const char *rel; /* directory this ignore governs, relative to root */ struct GitIgnore *parent; struct GitIgnore *gnext; /* global list, for cleanup */ } GitIgnore; static GitIgnore *gi_all = NULL; /* ---------- directory work queue (parallel walk + scan) ---------- */ typedef struct DirJob { char *path; /* full path for opendir/open */ char *rel; /* path relative to the search root ("" for root) */ GitIgnore *gi; /* active ignore set for this directory's children */ 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, const char *rel, GitIgnore *gi) { DirJob *j = malloc(sizeof *j); if(j == NULL) return; j->path = strdup(path); if(j->path == NULL) { free(j); return; } j->rel = strdup(rel); if(j->rel == NULL) { free(j->path); free(j); return; } j->gi = gi; 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 void pop_dir(char **path, char **rel, GitIgnore **gi) { pthread_mutex_lock(&dir_mx); DirJob *j = dir_stack; if(j != NULL) dir_stack = j->next; pthread_mutex_unlock(&dir_mx); if(j == NULL) { *path = NULL; *rel = NULL; *gi = NULL; return; } *path = j->path; *rel = j->rel; *gi = j->gi; free(j); } /* Read an entire fd into a malloc'd, NUL-terminated buffer. Returns the buffer * (caller frees) and stores the length in *len, or NULL on error. */ static char *read_all(int fd, size_t *len) { size_t cap = 4096, n = 0; char *buf = malloc(cap); if(buf == NULL) return NULL; for(;;) { if(n == cap) { cap *= 2; char *nb = realloc(buf, cap); if(nb == NULL) { free(buf); return NULL; } buf = nb; } ssize_t r = read(fd, buf + n, cap - n); if(r < 0) { if(errno == EINTR) continue; free(buf); return NULL; } if(r == 0) break; n += (size_t)r; } buf = realloc(buf, n + 1); if(buf == NULL) return NULL; buf[n] = '\0'; *len = n; return buf; } /* Parse a .gitignore buffer into patterns, chained under `parent`. Returns the * new GitIgnore node (sharing `parent` if the file is empty/absent). */ static GitIgnore *load_gitignore(const char *dir, GitIgnore *parent, const char *rel) { char path[PATH_MAX]; int n = snprintf(path, sizeof path, "%s/.gitignore", dir); if(n < 0 || (size_t)n >= sizeof path) return parent; int fd = open(path, O_RDONLY | O_CLOEXEC); if(fd < 0) return parent; size_t len = 0; char *buf = read_all(fd, &len); close(fd); if(buf == NULL) return parent; Pattern *pa = NULL; size_t np = 0, cap = 0; char *p = buf, *end = buf + len; while(p < end) { char *nl = memchr(p, '\n', (size_t)(end - p)); size_t llen = (nl != NULL) ? (size_t)(nl - p) : (size_t)(end - p); if(llen > 0 && p[llen - 1] == '\r') llen--; size_t s = 0; while(s < llen && (p[s] == ' ' || p[s] == '\t')) s++; size_t e = llen; while(e > s && (p[e - 1] == ' ' || p[e - 1] == '\t')) e--; if(s >= e || p[s] == '#') { p = (nl != NULL) ? nl + 1 : end; continue; } char *t = malloc(e - s + 1); if(t == NULL) break; memcpy(t, p + s, e - s); t[e - s] = '\0'; int negate = 0, dir_only = 0; if(t[0] == '!') { negate = 1; memmove(t, t + 1, strlen(t)); } size_t tl = strlen(t); if(tl > 0 && t[tl - 1] == '/') { dir_only = 1; t[tl - 1] = '\0'; tl--; } int has_slash = (strchr(t, '/') != NULL); if(np == cap) { cap = cap ? cap * 2 : 16; Pattern *na = realloc(pa, cap * sizeof *pa); if(na == NULL) { free(t); break; } pa = na; } pa[np].negate = negate; pa[np].dir_only = dir_only; pa[np].has_slash = has_slash; pa[np].pat = t; pa[np].pat_len = tl; np++; p = (nl != NULL) ? nl + 1 : end; } free(buf); if(np == 0) { free(pa); return parent; } GitIgnore *g = malloc(sizeof *g); if(g == NULL) { for(size_t i = 0; i < np; i++) free(pa[i].pat); free(pa); return parent; } g->pats = pa; g->npats = np; g->rel = strdup(rel); g->parent = parent; g->gnext = gi_all; gi_all = g; return g; } /* Match a single path segment `s` (len sl) against a glob `p` (len pl) where * '*' matches a run of non-'/' chars and '?' matches one char. */ static int seg_eq(const char *p, size_t pl, const char *s, size_t sl) { size_t i = 0, j = 0; size_t star = SIZE_MAX, si = 0, sj = 0; while(i < pl || j < sl) { if(i < pl && p[i] == '*') { star = i; si = i + 1; sj = j; i++; continue; } if(i < pl && (p[i] == '?' || (j < sl && p[i] == s[j]))) { i++; j++; continue; } if(star != SIZE_MAX) { /* Extend what the '*' matches by one more char of s, if possible. */ if(sj + 1 > sl) break; i = si; sj++; j = sj; continue; } return 0; } return i == pl && j == sl; } /* Match a slash-separated pattern `p` against path `s`, supporting `**`. */ static int match_path(const char *p, const char *s) { for(;;) { if(p[0] == '*' && p[1] == '*') { p += 2; while(*p == '/') p++; if(*p == '\0') return 1; const char *sp = s; for(;;) { if(match_path(p, sp)) return 1; while(*sp && *sp != '/') sp++; if(*sp == '\0') break; sp++; } return 0; } if(*p == '\0') return *s == '\0'; if(*p == '/') { if(*s != '/') return 0; p++; s++; continue; } const char *pe = p; while(*pe && *pe != '/') pe++; const char *se = s; while(*se && *se != '/') se++; if(!seg_eq(p, (size_t)(pe - p), s, (size_t)(se - s))) return 0; p = pe; s = se; } } /* Match a gitignore pattern against a path (path relative to the owning * .gitignore directory). A leading '/' only marks the pattern as anchored to * that directory (it is already implied by `path`), so it is stripped before * matching. Falls back to fnmatch for patterns without `**`. */ static int git_match(const char *pat, const char *path) { while(*pat == '/') pat++; if(*pat == '\0') return *path == '\0'; if(strstr(pat, "**") == NULL) return fnmatch(pat, path, FNM_PATHNAME) == 0; return match_path(pat, path); } /* Decide whether an entry named `name` (full relative path `entry_rel`, a * directory iff `is_dir`) is ignored by the active ignore set `gi`. The * deepest matching rule wins (git semantics). */ static int is_ignored(const char *entry_rel, const char *name, GitIgnore *gi, int is_dir) { GitIgnore *chain[1024]; int n = 0; for(GitIgnore *g = gi; g != NULL && n < 1024; g = g->parent) chain[n++] = g; int ignored = 0; for(int i = n - 1; i >= 0; i--) { /* root -> child (deepest wins) */ GitIgnore *g = chain[i]; for(size_t p = 0; p < g->npats; p++) { Pattern *pat = &g->pats[p]; int m; if(pat->has_slash) { const char *rp; if(g->rel[0] == '\0') { rp = entry_rel; } else { size_t rl = strlen(g->rel); if(strncmp(entry_rel, g->rel, rl) != 0) continue; if(entry_rel[rl] != '/') continue; rp = entry_rel + rl + 1; } m = git_match(pat->pat, rp); } else { if(pat->dir_only && !is_dir) continue; m = git_match(pat->pat, name); } if(m) ignored = pat->negate ? 0 : 1; } } return ignored; } /* ---------- 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]; char relbuf[PATH_MAX]; for(;;) { char *dir, *rel; GitIgnore *parent_gi; pop_dir(&dir, &rel, &parent_gi); if(dir == NULL) break; GitIgnore *my_gi = parent_gi; if(!git_all) my_gi = load_gitignore(dir, parent_gi, rel); 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; const char *name = ent->d_name; int n = snprintf(child, sizeof child, "%s/%s", dir, name); if(n < 0 || (size_t)n >= sizeof child) continue; n = snprintf(relbuf, sizeof relbuf, "%s%s%s", rel, (rel[0] == '\0') ? "" : "/", name); if(n < 0 || (size_t)n >= sizeof relbuf) continue; int is_dir = (ent->d_type == DT_DIR); if(ent->d_type == DT_UNKNOWN) { struct stat st; if(lstat(child, &st) == 0) is_dir = S_ISDIR(st.st_mode); } if(!git_all) { /* Always skip .git directories, just like git. */ if(strcmp(name, ".git") == 0 && is_dir) continue; if(is_ignored(relbuf, name, my_gi, is_dir)) continue; } if(ent->d_type == DT_REG) { scan_file(child, &sb); } else if(ent->d_type == DT_DIR) { push_dir(child, relbuf, my_gi); } 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, relbuf, my_gi); else if(S_ISREG(st.st_mode)) scan_file(child, &sb); } } closedir(d); } free(dir); free(rel); } free(sb.buf); free(sb.carry); return NULL; } static void free_gitignores(void) { GitIgnore *g = gi_all; while(g != NULL) { GitIgnore *next = g->gnext; for(size_t i = 0; i < g->npats; i++) free(g->pats[i].pat); free(g->pats); free((void *)g->rel); free(g); g = next; } gi_all = 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; } git_all = (getenv("GG_GIT_ALL") != NULL); 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, "", NULL); } 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); free_gitignores(); return found_any ? 0 : 1; }