brep stable
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# brep - minimal grep-like utility built on POSIX mmap() + SIMD + threads
|
||||
#
|
||||
# Build: make
|
||||
# Clean: make clean
|
||||
# Install: make install PREFIX=/usr/local
|
||||
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O2 -std=c11 -Wall -Wextra
|
||||
CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
|
||||
LDFLAGS ?=
|
||||
LDLIBS += -lpthread
|
||||
# Enable AVX2 for the SIMD matcher (needs a 2008+ Intel/AMD CPU).
|
||||
# Drop -mavx2 if targeting older hardware; the matcher still compiles.
|
||||
MARCH ?= -mavx2
|
||||
|
||||
BINARY := brep
|
||||
SRC := brep.c
|
||||
PREFIX ?= /usr/local
|
||||
BINDIR := $(PREFIX)/bin
|
||||
|
||||
all: $(BINARY)
|
||||
|
||||
$(BINARY): $(SRC)
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) $(MARCH) $(LDFLAGS) -o $@ $(SRC) $(LDLIBS)
|
||||
|
||||
clean:
|
||||
rm -f $(BINARY)
|
||||
|
||||
install: $(BINARY)
|
||||
install -d $(BINDIR)
|
||||
install -m 0755 $(BINARY) $(BINDIR)/$(BINARY)
|
||||
|
||||
uninstall:
|
||||
rm -f $(BINDIR)/$(BINARY)
|
||||
|
||||
.PHONY: all clean install uninstall
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user