aboutsummaryrefslogtreecommitdiff

// // bp.c - Source code for the bp parser // // See man ./bp.1 for more details //

#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include

#include “files.h” #include “match.h” #include “pattern.h” #include “printmatch.h” #include “utils.h”

#ifndef BPNAME #define BPNAME “bp” #endif

static const char *description = BPNAME “ - a Parsing Expression Grammar command line tool”; static const char *usage = (“Usage:\n” “ “ BPNAME “ [flags] […]\n\n” “Flags:\n” “ -A –context-after set number of lines of context to print after the match\n” “ -B –context-before set number of lines of context to print before the match\n” “ -C –context set number of lines of context to print before and after the match\n” “ -G –git in a git repository, treat filenames as patterns for git ls-files\n” “ -I –inplace modify a file in-place\n” “ -c –case use case sensitivity\n” “ -e –explain explain the matches\n” “ -f –format fancy|plain|bare|file:line set the output format\n” “ -g –grammar use the specified file as a grammar\n” “ -h –help print the usage and quit\n” “ -i –ignore-case preform matching case-insensitively\n” “ -l –list-files list filenames only\n” “ -r –replace replace the input pattern with the given replacement\n” “ -s –skip skip over the given pattern when looking for matches\n” “ -v –verbose print verbose debugging info\n” “ -w –word find words matching the given string pattern\n”);

// Used as a heuristic to check if a file is binary or text: #define CHECKFIRSTN_BYTES 256

#define USEDEFAULTCONTEXT -3 #define ALLCONTEXT -2 #define NOCONTEXT -1

// Flag-configurable options: static struct { int contextbefore, contextafter; bool ignorecase, verbose, gitmode, printfilenames; enum { MODENORMAL, MODELISTFILES, MODEINPLACE, MODEEXPLAIN } mode; enum { FORMATAUTO, FORMATFANCY, FORMATPLAIN, FORMATBARE, FORMATFILELINE } format; bppatt *skip; } options = { .contextbefore = USEDEFAULTCONTEXT, .contextafter = USEDEFAULTCONTEXT, .ignorecase = false, .printfilenames = true, .verbose = false, .mode = MODENORMAL, .format = FORMAT_AUTO, .skip = NULL, };

const char *LINEFORMATS[] = { [FORMATFANCY] = “\033[0;2m#\033(0\x78\033(B”, [FORMATPLAIN] = “#|”, [FORMATBARE] = ““, [FORMATFILELINE] = “@:#0:”, };

// If a file is partly through being modified when the program exits, restore it from backup. static FILE *modifyingfile = NULL; static filet *backup_file;

// // Helper function to reduce code duplication // static inline void fprintfilename(FILE *out, const char *filename) { if (!filename[0]) return; if (options.format == FORMATFANCY) fprintf(out, “\033[0;1;4;33m%s\033[m\n”, filename); else fprintf(out, “%s:\n”, filename); }

static void *portablememrchr(const void *s, int c, sizet n) { const unsigned char *p = (const unsigned char )s + n; while (n–) { if (–p == (unsigned char)c) return (void *)p; } return NULL; }

// // If there was a parse error while building a pattern, print an error message and exit. // static inline bppatt *assertpat(const char *start, const char *end, maybepatt maybepat) { if (!end) end = start + strlen(start); if (!maybepat.success) { const char *errstart = maybepat.value.error.start, *errend = maybepat.value.error.end, *errmsg = maybe_pat.value.error.msg;

    const char *nl = portable_memrchr(start, '\n', (size_t)(err_start - start));
    const char *sol = nl ? nl + 1 : start;
    nl = memchr(err_start, '\n', (size_t)(end - err_start));
    const char *eol = nl ? nl : end;
    if (eol < err_end) err_end = eol;

    fprintf(stderr, "\033[31;1m%s\033[0m\n", err_msg);
    fprintf(stderr, "%.*s\033[41;30m%.*s\033[m%.*s\n", (int)(err_start - sol), sol, (int)(err_end - err_start),
            err_start, (int)(eol - err_end), err_end);
    fprintf(stderr, "\033[34;1m");
    const char *p = sol;
    for (; p < err_start; ++p)
        (void)fputc(*p == '\t' ? '\t' : ' ', stderr);
    if (err_start == err_end) ++err_end;
    for (; p < err_end; ++p)
        if (*p == '\t')
            // Some janky hacks: 8 ^'s, backtrack 8 spaces, move forward a tab stop, clear any ^'s that overshot
            fprintf(stderr, "^^^^^^^^\033[8D\033[I\033[K");
        else (void)fputc('^', stderr);
    fprintf(stderr, "\033[m\n");
    exit(EXIT_FAILURE);
}
return maybe_pat.value.pat;

}

// // Look for a key/value flag at the first position in the given argument list. // If the flag is found, update next to point to the next place to check for a flag. // The contents of argv[0] may be modified for single-char flags. // Return the flag’s value. // attribute((nonnull)) static char *getflag(char *argv[], const char *flag, char ***next) { sizet n = strlen(flag); if (strncmp(argv[0], flag, n) != 0) return NULL; if (argv[0][n] == ‘=’) { // –foo=baz, -f=baz *next = &argv[1]; return &argv[0][n + 1]; } else if (argv[0][n] == ‘\0’) { // –foo baz, -f baz if (!argv[1]) errx(EXIT_FAILURE, “Expected argument after ‘%s’\n\n%s”, flag, usage); *next = &argv[2]; return argv[1]; } else if (flag[0] == ‘-’ && flag[1] != ‘-’ && flag[2] == ‘\0’) { // -f… *next = &argv[1]; return &argv[0][n]; } return NULL; }

// // Look for a flag at the first position in the given argument list. // If the flag is found, update next to point to the next place to check for a flag. // The contents of argv[0] may be modified for single-char flags. // Return a boolean for whether or not the flag was found. // attribute((nonnull)) static bool getboolflag(char *argv[], const char *flag, char ***next) { sizet n = strlen(flag); if (strncmp(argv[0], flag, n) != 0) return false; if (argv[0][n] == ‘\0’) { // –foo, -f *next = &argv[1]; return true; } else if (flag[0] == ‘-’ && flag[1] != ‘-’ && flag[2] == ‘\0’) { // -f… memmove(&argv[0][1], &argv[0][2], 1 + strlen(&argv[0][2])); // Shift the flags down *next = argv; return true; } return false; }

// // Scan the first few dozen bytes of a file and return 1 if the contents all // look like printable text characters, otherwise return 0. // static int istextfile(const char *filename) { int fd = open(filename, ORDONLY); if (fd < 0) return 0; char buf[CHECKFIRSTNBYTES]; ssizet len = read(fd, buf, sizeof(buf) / sizeof(char)); (void)close(fd); if (len < 0) return 0; for (ssizet i = 0; i < len; i++) if (isascii(buf[i]) && !(isprint(buf[i]) || isspace(buf[i]))) return 0; return 1; }

// // Print matches in a visual explanation style // static int explainmatches(filet *f, bppatt *pattern, bppatt *defs) { int nmatches = 0; for (bpmatcht *m = NULL; nextmatch(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase);) { if (++nmatches == 1) { if (options.printfilenames) fprintfilename(stdout, f->filename); } else printf(“\n\n”); explainmatch(m); } return nmatches; }

// // Cleanup function to ensure no temp files are left around if the program // exits unexpectedly. // static void cleanup(void) { if (modifyingfile && backupfile) { rewind(modifyingfile); ftruncate(fileno(modifyingfile), 0); (void)fwrite(backupfile->start, 1, (sizet)(backupfile->end - backupfile->start), modifyingfile); fclose(modifyingfile); modifyingfile = NULL; } if (backupfile) destroyfile(&backupfile); }

// // Signal handler to ensure cleanup happens. // static void sig_handler(int sig) { cleanup(); if (kill(0, sig)) exit(EXITFAILURE); }

int fprintlinenum(FILE *out, filet *f, int linenum, const char normalcolor) { int printed = 0; switch (options.format) { case FORMATFANCY: case FORMATPLAIN: { int space = 0; for (int i = (int)f->nlines; i > 0; i /= 10) ++space; if (options.format == FORMATFANCY) printed += fprintf(out, “\033[0;2m%d\033(0\x78\033(B%s”, space, linenum, normalcolor ? normalcolor : ““); else printed += fprintf(out, “%*d|”, space, linenum); break; } case FORMATFILELINE: { printed += fprintf(out, “%s:%d:”, f->filename, linenum); break; } default: break; } return printed; }

static filet *printingfile = NULL; static int lastlinenum = -1; static int fprintbetween(FILE *out, const char *start, const char *end, const char *normalcolor) { int printed = 0; do { // Cheeky lookbehind to see if line number should be printed if (start == printingfile->start || start[-1] == ‘\n’) { int linenum = (int)getlinenumber(printingfile, start); if (lastlinenum != linenum) { printed += fprintlinenum(out, printingfile, linenum, normalcolor); lastlinenum = linenum; } } const char *lineend = memchr(start, ‘\n’, (sizet)(end - start)); if (lineend && lineend != end) { printed += fwrite(start, sizeof(char), (sizet)(lineend - start + 1), out); start = lineend + 1; } else { if (end > start) printed += fwrite(start, sizeof(char), (sizet)(end - start), out); break; } } while (start < end); return printed; }

static void fprintcontext(FILE *out, filet *f, const char *prev, const char *next) { if (options.contextbefore == ALLCONTEXT || options.contextafter == ALLCONTEXT) { fprintbetween(out, prev ? prev : f->start, next ? next : f->end, “\033[m”); return; } const char *beforenext = next; if (next && options.contextbefore >= 0) { sizet linebeforenext = getlinenumber(printingfile, next); linebeforenext = options.contextbefore >= (int)linebeforenext ? 1 : linebeforenext - (sizet)options.contextbefore; beforenext = getline(printingfile, linebeforenext); if (prev && beforenext < prev) beforenext = prev; } const char *afterprev = prev; if (prev && options.contextafter >= 0) { sizet lineafterprev = getlinenumber(printingfile, prev) + (sizet)options.contextafter + 1; afterprev = lineafterprev > printingfile->nlines ? printingfile->end : getline(printingfile, lineafterprev > printingfile->nlines ? printingfile->nlines : lineafterprev); if (next && afterprev > next) afterprev = next; } if (next && prev && afterprev >= before_next) { fprintbetween(out, prev, next, “\033[m”); } else { if (prev) fprintbetween(out, prev, after_prev, “\033[m”); if (next) fprintbetween(out, before_next, next, “\033[m”); } }

static void onnl(FILE *out) { switch (options.format) { case FORMATFANCY: case FORMATPLAIN: for (int i = (int)printingfile->nlines; i > 0; i /= 10) fputc(‘.’, out); fprintf(out, “%s”, options.format == FORMAT_FANCY ? “\033[0;2m\033(0\x78\033(B\033[m” : “|”); break; default: break; } }

// // Print all the matches in a file. // static int printmatches(FILE *out, filet *f, bppatt *pattern, bppatt *defs) { static int printed_filenames = 0; int matches = 0; const char *prev = NULL;

printing_file = f;
last_line_num = -1;

print_options_t print_opts = {.fprint_between = _fprint_between, .on_nl = on_nl};
if (options.format == FORMAT_FANCY) {
    print_opts.match_color = "\033[0;31;1m";
    print_opts.replace_color = "\033[0;34;1m";
    print_opts.normal_color = "\033[m";
}
for (bp_match_t *m = NULL; next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase);) {
    if (++matches == 1 && options.print_filenames) {
        if (printed_filenames++ > 0) printf("\n");
        fprint_filename(out, f->filename);
    }
    fprint_context(out, f, prev, m->start);
    if (print_opts.normal_color) fprintf(out, "%s", print_opts.normal_color);
    fprint_match(out, f->start, m, &print_opts);
    if (print_opts.normal_color) fprintf(out, "%s", print_opts.normal_color);
    prev = m->end;
}
// Print trailing context if needed:
if (matches > 0) {
    fprint_context(out, f, prev, NULL);
    if (last_line_num < 0) { // Hacky fix to ensure line number gets printed for `bp '{$$}'`
        fprint_linenum(out, f, f->nlines, print_opts.normal_color);
        fputc('\n', out);
    }
}

printing_file = NULL;
last_line_num = -1;
return matches;

}

// // For a given filename, open the file and attempt to match the given pattern // against it, printing any results according to the flags. // attribute((nonnull)) static int processfile(const char *filename, bppatt *pattern, bppatt *defs) { filet *f = load_file(NULL, filename); if (f == NULL) { fprintf(stderr, “Could not open file: %s\n%s\n”, filename, strerror(errno)); return 0; }

int matches = 0;
if (options.mode == MODE_EXPLAIN) {
    matches += explain_matches(f, pattern, defs);
} else if (options.mode == MODE_LISTFILES) {
    bp_match_t *m = NULL;
    if (next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase)) {
        printf("%s\n", f->filename);
        matches += 1;
    }
    stop_matching(&m);
} else if (options.mode == MODE_INPLACE) {
    bp_match_t *m = NULL;
    bool found = next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase);
    stop_matching(&m);
    if (!found) return 0;

    // Ensure the file is resident in memory:
    if (f->mmapped) {
        file_t *copy = spoof_file(NULL, f->filename, f->start, (ssize_t)(f->end - f->start));
        destroy_file(&f);
        f = copy;
    }
    FILE *out = fopen(filename, "w");
    // Set these temporary values in case the program crashes while in the
    // middle of inplace modifying a file. If that happens, these variables
    // are used to restore the original file contents.
    modifying_file = out;
    backup_file = f;
    {
        matches += print_matches(out, f, pattern, defs);
    }
    modifying_file = NULL;
    backup_file = NULL;
    fclose(out);
    if (matches > 0)
        printf(getenv("NO_COLOR") ? "%s: %d replacement%s\n" : "\x1b[33;1m%s:\x1b[m %d replacement%s\n", filename,
               matches, matches == 1 ? "" : "s");
} else {
    matches += print_matches(stdout, f, pattern, defs);
}
fflush(stdout);

if (recycle_all_matches() != 0)
    fprintf(stderr, "\033[33;1mMemory leak: there should no longer be any matches in use at this point.\033[m\n");
destroy_file(&f);
(void)fflush(stdout);
return matches;

}

// // Recursively process all non-dotfile files in the given directory. // attribute((nonnull)) static int processdir(const char *dirname, bppatt *pattern, bppatt *defs) { int matches = 0; globt globbuf; char globpath[PATHMAX + 1] = {’\0’}; if (snprintf(globpath, PATHMAX, “%s/”, dirname) > (int)PATHMAX) errx(EXITFAILURE, “Filename is too long: %s/”, dirname); int status = glob(globpath, 0, NULL, &globbuf); if (status == GLOBABORTED || status == GLOBNOSPACE) errx(EXITFAILURE, “Failed to get directory contents: %s”, dirname); if (status != GLOBNOMATCH) { struct stat statbuf; for (sizet i = 0; i < globbuf.glpathc; i++) { if (lstat(globbuf.glpathv[i], &statbuf) != 0) continue; if (SISLNK(statbuf.stmode)) continue; // Skip symbolic links else if (SISDIR(statbuf.stmode)) matches += processdir(globbuf.glpathv[i], pattern, defs); else if (istextfile(globbuf.glpathv[i])) matches += processfile(globbuf.glpathv[i], pattern, defs); } } globfree(&globbuf); return matches; }

// // Process git files using git ls-files ... // attribute((nonnull(1))) static int processgitfiles(bppatt *pattern, bppatt *defs, int argc, char *argv[]) { int fds[2]; require(pipe(fds), “Failed to create pipe”); pidt child = require(fork(), “Failed to fork”); if (child == 0) { const char **gitargs = new (char * [3 + argc + 1]); int g = 0; gitargs[g++] = “git”; gitargs[g++] = “ls-files”; gitargs[g++] = “-z”; while (*argv) gitargs[g++] = *(argv++); require(dup2(fds[STDOUTFILENO], STDOUTFILENO), “Failed to hook up pipe to stdout”); require(close(fds[STDINFILENO]), “Failed to close read end of pipe”); (void)execvp(“git”, (char **)gitargs); exit(EXITFAILURE); } require(close(fds[STDOUTFILENO]), “Failed to close write end of pipe”); FILE *fp = require(fdopen(fds[STDINFILENO], “r”), “Could not open pipe file descriptor”); char *path = NULL; sizet pathsize = 0; int found = 0; while (getdelim(&path, &pathsize, ‘\0’, fp) > 0) found += processfile(path, pattern, defs); if (path) delete (&path); require(fclose(fp), “Failed to close read end of pipe”); int status; while (waitpid(child, &status, 0) != child) continue; if (!((WIFEXITED(status) == 1) && (WEXITSTATUS(status) == 0))) errx(EXIT_FAILURE, “git ls-files -z failed.”); return found; }

// // Load the given grammar (semicolon-separated definitions) // and return the first rule defined. // static bppatt *loadgrammar(bppatt *defs, filet *f) { return chaintogether(defs, assertpat(f->start, f->end, bp_pattern(f->start, f->end))); }

// // Convert a context string to an integer // static int contextfromflag(const char *flag) { if (streq(flag, “all”)) return ALLCONTEXT; if (streq(flag, “none”)) return NOCONTEXT; return (int)strtol(flag, NULL, 10); }

// // Check if any letters are uppercase // static bool any_uppercase(const char *str) { for (; str; ++str) { if (isupper(str)) return true; } return false; }

#define FLAG(f) (flag = getflag(argv, f, &argv)) #define BOOLFLAG(f) getboolflag(argv, f, &argv)

int main(int argc, char *argv[]) { char *flag = NULL;

bp_pat_t *defs = NULL;
file_t *loaded_files = NULL;
bp_pat_t *pattern = NULL;

// Load builtins:
file_t *builtins_file = load_file(&loaded_files, BP_PREFIX "/" BP_NAME "/builtins.bp");
if (builtins_file) defs = load_grammar(defs, builtins_file);
file_t *local_file = load_file(&loaded_files, BP_PREFIX "/share/" BP_NAME "/grammars/builtins.bp");
if (local_file) defs = load_grammar(defs, local_file);

bool explicit_case_sensitivity = false;

++argv; // skip program name
while (argv[0]) {
    if (streq(argv[0], "--")) {
        ++argv;
        break;
    } else if (BOOLFLAG("-h") || BOOLFLAG("--help")) {
        printf("%s\n\n%s", description, usage);
        exit(EXIT_SUCCESS);
    } else if (BOOLFLAG("-v") || BOOLFLAG("--verbose")) {
        options.verbose = true;
    } else if (BOOLFLAG("-e") || BOOLFLAG("--explain")) {
        options.mode = MODE_EXPLAIN;
    } else if (BOOLFLAG("-I") || BOOLFLAG("--inplace")) {
        options.mode = MODE_INPLACE;
        options.print_filenames = false;
        options.format = FORMAT_BARE;
    } else if (BOOLFLAG("-G") || BOOLFLAG("--git")) {
        options.git_mode = true;
    } else if (BOOLFLAG("-i") || BOOLFLAG("--ignore-case")) {
        options.ignorecase = true;
        explicit_case_sensitivity = true;
    } else if (BOOLFLAG("-c") || BOOLFLAG("--case")) {
        options.ignorecase = false;
        explicit_case_sensitivity = true;
    } else if (BOOLFLAG("-l") || BOOLFLAG("--list-files")) {
        options.mode = MODE_LISTFILES;
    } else if (FLAG("-r") || FLAG("--replace")) {
        if (!pattern) errx(EXIT_FAILURE, "No pattern has been defined for replacement to operate on");
        // TODO: spoof file as sprintf("pattern => '%s'", flag)
        // except that would require handling edge cases like quotation marks etc.
        pattern = assert_pat(flag, NULL, bp_replacement(pattern, flag, flag + strlen(flag)));
        if (options.context_before == USE_DEFAULT_CONTEXT) options.context_before = ALL_CONTEXT;
        if (options.context_after == USE_DEFAULT_CONTEXT) options.context_after = ALL_CONTEXT;
    } else if (FLAG("-g") || FLAG("--grammar")) {
        file_t *f = NULL;
        if (strlen(flag) > 3 && strncmp(&flag[strlen(flag) - 3], ".bp", 3) == 0) f = load_file(&loaded_files, flag);
        if (f == NULL) f = load_filef(&loaded_files, "%s/.config/" BP_NAME "/%s.bp", getenv("HOME"), flag);
        if (f == NULL) f = load_filef(&loaded_files, BP_PREFIX "/share/" BP_NAME "/grammars/%s.bp", flag);
        if (f == NULL) errx(EXIT_FAILURE, "Couldn't find grammar: %s", flag);
        defs = load_grammar(defs, f); // Keep in memory for debug output
    } else if (FLAG("-w") || FLAG("--word")) {
        require(asprintf(&flag, "{|}%s{|}", flag), "Could not allocate memory");
        file_t *arg_file = spoof_file(&loaded_files, "<word pattern>", flag, -1);
        if (!explicit_case_sensitivity) options.ignorecase = !any_uppercase(flag);
        delete (&flag);
        bp_pat_t *p = assert_pat(arg_file->start, arg_file->end, bp_stringpattern(arg_file->start, arg_file->end));
        pattern = chain_together(pattern, p);
    } else if (FLAG("-s") || FLAG("--skip")) {
        bp_pat_t *s = assert_pat(flag, NULL, bp_pattern(flag, flag + strlen(flag)));
        options.skip = either_pat(options.skip, s);
    } else if (FLAG("-C") || FLAG("--context")) {
        options.context_before = options.context_after = context_from_flag(flag);
    } else if (FLAG("-B") || FLAG("--before-context")) {
        options.context_before = context_from_flag(flag);
    } else if (FLAG("-A") || FLAG("--after-context")) {
        options.context_after = context_from_flag(flag);
    } else if (FLAG("-f") || FLAG("--format")) {
        if (streq(flag, "fancy")) options.format = FORMAT_FANCY;
        else if (streq(flag, "plain")) options.format = FORMAT_PLAIN;
        else if (streq(flag, "bare")) options.format = FORMAT_BARE;
        else if (streq(flag, "file:line")) {
            options.format = FORMAT_FILE_LINE;
            options.print_filenames = 0;
        } else if (!streq(flag, "auto")) errx(EXIT_FAILURE, "Unknown --format option: %s", flag);
    } else if (argv[0][0] != '-' || strncmp(argv[0], "->", 2) == 0) {
        // As a special case, support `bp '->foo'` as a way to search for
        // pointer field accesses without needing to escape anything.
        if (pattern != NULL) break;
        bp_pat_t *p = assert_pat(argv[0], NULL, bp_stringpattern(argv[0], argv[0] + strlen(argv[0])));
        if (!explicit_case_sensitivity) options.ignorecase = !any_uppercase(argv[0]);
        pattern = chain_together(pattern, p);
        ++argv;
    } else {
        errx(EXIT_FAILURE, "Unrecognized flag: %s\n\n%s", argv[0], usage);
    }
}

if (pattern == NULL) errx(EXIT_FAILURE, "No pattern provided.\n\n%s", usage);

for (argc = 0; argv[argc]; ++argc)
    ; // update argc

if (options.context_before == USE_DEFAULT_CONTEXT) options.context_before = 0;
if (options.context_after == USE_DEFAULT_CONTEXT) options.context_after = 0;

if (options.format == FORMAT_AUTO)
    options.format = isatty(STDOUT_FILENO) ? (getenv("NO_COLOR") ? FORMAT_PLAIN : FORMAT_FANCY) : FORMAT_BARE;

// If any of these signals triggers, and there is a temporary file in use,
// be sure to clean it up before exiting.
int signals[] = {SIGTERM, SIGINT, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGSEGV, SIGTSTP};
struct sigaction sa = {.sa_handler = &sig_handler, .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
for (size_t i = 0; i < sizeof(signals) / sizeof(signals[0]); i++)
    require(sigaction(signals[i], &sa, NULL), "Failed to set signal handler");

// Handle exit() calls gracefully:
require(atexit(&cleanup), "Failed to set cleanup handler at exit");

if (options.verbose) {
    fputs("Matching pattern: ", stderr);
    fprint_pattern(stderr, pattern);
    fputc('\n', stderr);
}

// Default to git mode if there's a .git directory and no files were specified:
struct stat gitdir;
if (argc == 0 && stat(".git", &gitdir) == 0 && S_ISDIR(gitdir.st_mode)) options.git_mode = true;

int found = 0;
if (!isatty(STDIN_FILENO) && !argv[0]) {
    // Piped in input:
    options.print_filenames = false; // Don't print filename on stdin
    found += process_file("", pattern, defs);
} else if (options.git_mode) {
    // Get the list of files from `git --ls-files ...`
    found = process_git_files(pattern, defs, argc, argv);
} else if (argv[0]) {
    // Files passed in as command line args:
    struct stat statbuf;
    if (!argv[1]
        && !(stat(argv[0], &statbuf) == 0
             && S_ISDIR(statbuf.st_mode))) // Don't print filename for single-file matching
        options.print_filenames = false;
    for (; argv[0]; argv++) {
        if (stat(argv[0], &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) // Symlinks are okay if manually specified
            found += process_dir(argv[0], pattern, defs);
        else found += process_file(argv[0], pattern, defs);
    }
} else {
    // No files, no piped in input, so use files in current dir, recursively
    found += process_dir(".", pattern, defs);
}

// This code frees up all residual heap-allocated memory. Since the program
// is about to exit, this step is unnecessary. However, it is useful for
// tracking down memory leaks.
free_all_matches();
free_all_pats();
while (loaded_files) {
    file_t *next = loaded_files->next;
    destroy_file(&loaded_files);
    loaded_files = next;
}

exit(found > 0 ? EXIT_SUCCESS : EXIT_FAILURE);

}

// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,:0