aboutsummaryrefslogtreecommitdiff
path: root/src/grab.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/grab.c')
-rw-r--r--src/grab.c492
1 files changed, 492 insertions, 0 deletions
diff --git a/src/grab.c b/src/grab.c
new file mode 100644
index 0000000..96b5d9f
--- /dev/null
+++ b/src/grab.c
@@ -0,0 +1,492 @@
+#include <err.h>
+#include <getopt.h>
+#include <libgen.h>
+#include <limits.h>
+#include <locale.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#if !GRAB_IS_C23
+# include <assert.h>
+# include <stdbool.h>
+# define nullptr NULL
+#endif
+
+#include "da.h"
+
+#ifndef REG_STARTEND
+# error "REG_STARTEND not defined"
+#endif
+
+#define die(...) err(EXIT_FAILURE, __VA_ARGS__);
+#define diex(...) errx(EXIT_FAILURE, __VA_ARGS__);
+#define warn(...) \
+ do { \
+ warn(__VA_ARGS__); \
+ rv = EXIT_FAILURE; \
+ } while (0)
+#define streq(a, b) (!strcmp(a, b))
+
+#define EEARLY "Input string terminated prematurely"
+
+struct op {
+ char c;
+ regex_t pat;
+};
+
+struct ops {
+ struct op *buf;
+ size_t len, cap;
+};
+
+struct chars {
+ char *buf;
+ size_t len, cap;
+};
+
+struct sv {
+ char *p;
+ size_t len;
+};
+
+typedef unsigned char uchar;
+typedef void (*cmd_func)(struct sv, struct ops, size_t, const char *);
+
+static void cmdg(struct sv, struct ops, size_t, const char *);
+static void cmdx(struct sv, struct ops, size_t, const char *);
+static void cmdy(struct sv, struct ops, size_t, const char *);
+
+static void grab(struct ops, FILE *, const char *);
+static void putm(struct sv, const char *);
+static regex_t mkregex(char *, size_t);
+static struct ops comppat(char *);
+static char *env_or_default(const char *, const char *);
+#if GIT_GRAB
+static FILE *getfstream(int n, char *v[n]);
+#endif
+
+static bool xisspace(char);
+static char *xstrchrnul(const char *, char);
+
+static int filecnt, rv;
+static bool color, nflag, zflag;
+static bool fflag =
+#if GIT_GRAB
+ true;
+#else
+ false;
+#endif
+
+static const cmd_func op_table[UCHAR_MAX] = {
+ ['g'] = cmdg,
+ ['v'] = cmdg,
+ ['x'] = cmdx,
+ ['y'] = cmdy,
+};
+
+static const char esc_table[UCHAR_MAX] = {
+ ['\\'] = '\\', ['a'] = '\a', ['b'] = '\b', ['f'] = '\f',
+ ['n'] = '\n', ['r'] = '\r', ['t'] = '\t', ['v'] = '\v',
+};
+
+static void
+usage(const char *s)
+{
+ fprintf(stderr,
+#if GIT_GRAB
+ "Usage: %s [-nz] pattern [glob ...]\n"
+#else
+ "Usage: %s [-fnz] pattern [file ...]\n"
+#endif
+ " %s -h\n",
+ s, s);
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char **argv)
+{
+ int opt;
+ struct ops ops;
+ struct option longopts[] = {
+ {"filenames", no_argument, 0, 'f'},
+ {"help", no_argument, 0, 'h'},
+ {"newline", no_argument, 0, 'n'},
+ {"zero", no_argument, 0, 'z'},
+ };
+
+#if GIT_GRAB
+ char *entry = NULL;
+ size_t len;
+ ssize_t nr;
+ FILE *flist;
+ const char *opts = "hnz";
+#else
+ const char *opts = "fhnz";
+#endif
+
+ argv[0] = basename(argv[0]);
+ if (argc < 2)
+ usage(argv[0]);
+
+ setlocale(LC_ALL, "");
+
+ while ((opt = getopt_long(argc, argv, opts, longopts, nullptr)) != -1) {
+ switch (opt) {
+#if !GIT_GRAB
+ case 'f':
+ fflag = true;
+ break;
+#endif
+ case 'h':
+ execlp("man", "man", "1", argv[0], nullptr);
+ die("execlp: man 1 %s", argv[0]);
+ case 'n':
+ nflag = true;
+ break;
+ case 'z':
+ zflag = true;
+ break;
+ default:
+ usage(argv[0]);
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ filecnt = argc - 1;
+
+ if (isatty(STDOUT_FILENO) == 1 && !env_or_default("NO_COLOR", nullptr))
+ color = !streq(env_or_default("TERM", ""), "dumb");
+
+ ops = comppat(argv[0]);
+
+#if GIT_GRAB
+ if ((flist = getfstream(argc - 1, argv + 1)) == nullptr)
+ die("getfstream");
+ while ((nr = getdelim(&entry, &len, '\0', flist)) > 0) {
+ FILE *fp;
+
+ if ((fp = fopen(entry, "r")) == nullptr)
+ warn("fopen: %s", entry);
+ else {
+ grab(ops, fp, entry);
+ fclose(fp);
+ }
+ }
+ if (ferror(flist))
+ warn("getdelim");
+#else
+ if (argc == 1)
+ grab(ops, stdin, "-");
+ else {
+ for (int i = 1; i < argc; i++) {
+ FILE *fp;
+
+ if (streq(argv[i], "-")) {
+ grab(ops, stdin, "-");
+ } else if ((fp = fopen(argv[i], "r")) == nullptr) {
+ warn("fopen: %s", argv[i]);
+ } else {
+ grab(ops, fp, argv[i]);
+ fclose(fp);
+ }
+ }
+ }
+#endif
+
+#ifdef GRAB_DEBUG
+ for (size_t i = 0; i < ops.len; i++)
+ regfree(&ops.buf[i].pat);
+ free(ops.buf);
+#endif
+
+ return rv;
+}
+
+struct ops
+comppat(char *s)
+{
+#define skip_ws(p) for (; *(p) && xisspace(*(p)); (p)++)
+ struct ops ops;
+
+ da_init(&ops, 8);
+ skip_ws(s);
+ if (!*s)
+ diex(EEARLY);
+
+ do {
+ char delim;
+ char *p;
+ struct op op;
+
+ op.c = *s;
+ if (!op_table[(uchar)op.c])
+ diex("Invalid operator ā€˜%cā€™", *s);
+ if (!(delim = *++s))
+ diex(EEARLY);
+
+ p = ++s;
+ s = xstrchrnul(s, delim);
+ op.pat = mkregex(p, s - p);
+ da_append(&ops, op);
+
+ if (*s)
+ s++;
+ skip_ws(s);
+ } while (*s && *(s + 1));
+
+ return ops;
+#undef skip_ws
+}
+
+void
+grab(struct ops ops, FILE *stream, const char *filename)
+{
+ size_t n;
+ struct chars chars = {0};
+
+ do {
+ static_assert(sizeof(char) == 1, "sizeof(char) != 1; wtf?");
+ chars.cap += BUFSIZ;
+ if ((chars.buf = realloc(chars.buf, chars.cap)) == nullptr)
+ die("realloc");
+ chars.len += n = fread(chars.buf + chars.len, 1, BUFSIZ, stream);
+ } while (n == BUFSIZ);
+
+ if (ferror(stream))
+ warn("fread: %s", filename);
+ else {
+ struct sv sv = {
+ .p = chars.buf,
+ .len = chars.len,
+ };
+ op_table[(uchar)ops.buf[0].c](sv, ops, 0, filename);
+ }
+
+ free(chars.buf);
+}
+
+void
+cmdg(struct sv sv, struct ops ops, size_t i, const char *filename)
+{
+ int r;
+ regmatch_t pm = {
+ .rm_so = 0,
+ .rm_eo = sv.len,
+ };
+ struct op op = ops.buf[i];
+
+ r = regexec(&op.pat, sv.p, 1, &pm, REG_STARTEND);
+ if ((r == REG_NOMATCH && op.c == 'g') || (r != REG_NOMATCH && op.c == 'v'))
+ return;
+
+ if (i + 1 == ops.len)
+ putm(sv, filename);
+ else
+ op_table[(uchar)ops.buf[i + 1].c](sv, ops, i + 1, filename);
+}
+
+void
+cmdx(struct sv sv, struct ops ops, size_t i, const char *filename)
+{
+ regmatch_t pm = {
+ .rm_so = 0,
+ .rm_eo = sv.len,
+ };
+ struct op op = ops.buf[i];
+
+ do {
+ struct sv nsv;
+
+ if (regexec(&op.pat, sv.p, 1, &pm, REG_STARTEND) == REG_NOMATCH)
+ break;
+ nsv = (struct sv){.p = sv.p + pm.rm_so, .len = pm.rm_eo - pm.rm_so};
+ if (i + 1 == ops.len)
+ putm(nsv, filename);
+ else
+ op_table[(uchar)ops.buf[i + 1].c](nsv, ops, i + 1, filename);
+
+ if (pm.rm_so == pm.rm_eo)
+ pm.rm_eo++;
+ pm = (regmatch_t){
+ .rm_so = pm.rm_eo,
+ .rm_eo = sv.len,
+ };
+ } while (pm.rm_so < pm.rm_eo);
+}
+
+void
+cmdy(struct sv sv, struct ops ops, size_t i, const char *filename)
+{
+ regmatch_t pm = {
+ .rm_so = 0,
+ .rm_eo = sv.len,
+ };
+ regmatch_t prev = {
+ .rm_so = 0,
+ .rm_eo = 0,
+ };
+ struct op op = ops.buf[i];
+
+ do {
+ struct sv nsv;
+
+ if (regexec(&op.pat, sv.p, 1, &pm, REG_STARTEND) == REG_NOMATCH)
+ break;
+
+ if (prev.rm_so || prev.rm_eo || pm.rm_so) {
+ nsv = (struct sv){
+ .p = sv.p + prev.rm_eo,
+ .len = pm.rm_so - prev.rm_eo,
+ };
+ if (i + 1 == ops.len)
+ putm(nsv, filename);
+ else
+ op_table[(uchar)ops.buf[i + 1].c](nsv, ops, i + 1, filename);
+ }
+
+ prev = pm;
+ if (pm.rm_so == pm.rm_eo)
+ pm.rm_eo++;
+ pm = (regmatch_t){
+ .rm_so = pm.rm_eo,
+ .rm_eo = sv.len,
+ };
+ } while (pm.rm_so < pm.rm_eo);
+
+ if (prev.rm_eo < pm.rm_eo) {
+ struct sv nsv = {
+ .p = sv.p + pm.rm_so,
+ .len = pm.rm_eo - pm.rm_so,
+ };
+ if (i + 1 == ops.len)
+ putm(nsv, filename);
+ else
+ op_table[(uchar)ops.buf[i + 1].c](nsv, ops, i + 1, filename);
+ }
+}
+
+void
+putm(struct sv sv, const char *filename)
+{
+ static const char *fnc, *sepc;
+
+ if (!fnc) {
+ fnc = env_or_default("GRAB_COLOR_FNAME", "35");
+ sepc = env_or_default("GRAB_COLOR_SEP", "36");
+ }
+
+ if (fflag || filecnt > 1) {
+ if (color) {
+ printf("\33[%sm%s\33[%sm%c\33[0m", fnc, filename, sepc,
+ zflag ? '\0' : ':');
+ } else
+ printf("%s%c", filename, zflag ? '\0' : ':');
+ }
+ fwrite(sv.p, 1, sv.len, stdout);
+ putchar(zflag ? '\0' : '\n');
+}
+
+regex_t
+mkregex(char *s, size_t n)
+{
+ char c = s[n];
+ int ret, cflags;
+ regex_t r;
+
+ for (size_t i = 0; i < n - 1; i++) {
+ if (s[i] == '\\') {
+ char c = esc_table[(uchar)s[i + 1]];
+ if (c) {
+ for (size_t j = i; j < n - 1; j++)
+ s[j] = s[j + 1];
+ s[i] = c;
+ n--;
+ }
+ }
+ }
+
+ s[n] = 0;
+ cflags = REG_EXTENDED;
+ if (nflag)
+ cflags |= REG_NEWLINE;
+ if ((ret = regcomp(&r, s, cflags)) != 0) {
+ char emsg[128];
+ regerror(ret, &r, emsg, sizeof(emsg));
+ diex("Failed to compile regex: %s", emsg);
+ }
+ s[n] = c;
+
+ return r;
+}
+
+#if GIT_GRAB
+FILE *
+getfstream(int argc, char *argv[argc])
+{
+ pid_t pid;
+ int fds[2];
+ enum {
+ FD_R,
+ FD_W,
+ };
+
+ if (pipe(fds) == -1)
+ die("pipe");
+
+ switch (pid = fork()) {
+ case -1:
+ die("fork");
+ case 0:;
+ size_t len = argc + 5;
+ char **args;
+
+ close(fds[FD_R]);
+ if (dup2(fds[FD_W], STDOUT_FILENO) == -1)
+ die("dup2");
+
+ if (!(args = malloc(len * sizeof(char *))))
+ die("malloc");
+ args[0] = "git";
+ args[1] = "ls-files";
+ args[2] = "-z";
+ args[3] = "--";
+ memcpy(args + 4, argv, argc * sizeof(char *));
+ args[len - 1] = nullptr;
+
+ execvp("git", args);
+ die("execvp: git ls-files -z");
+ }
+
+ close(fds[FD_W]);
+ return fdopen(fds[FD_R], "r");
+}
+#endif
+
+char *
+env_or_default(const char *e, const char *d)
+{
+ const char *s = getenv(e);
+ return (char *)(s && *s ? s : d);
+}
+
+bool
+xisspace(char c)
+{
+ return c == ' ' || c == '\t' || c == '\n';
+}
+
+char *
+xstrchrnul(const char *s, char c)
+{
+ for (; *s; s++) {
+ if (*s == '\\')
+ s++;
+ else if (*s == c)
+ break;
+ }
+ return (char *)s;
+}