2 // bp.c - Source code for the bp parser
4 // See `man ./bp.1` for more details
19 #include <sys/types.h>
26 #include "printmatch.h"
33 static const char *description = BP_NAME " - a Parsing Expression Grammar command line tool";
34 static const char *usage =
36 " " BP_NAME " [flags] <pattern> [<files>...]\n\n"
38 " -A --context-after <n> set number of lines of context to print after the match\n"
39 " -B --context-before <n> set number of lines of context to print before the match\n"
40 " -C --context <context> set number of lines of context to print before and after the match\n"
41 " -G --git in a git repository, treat filenames as patterns for `git ls-files`\n"
42 " -I --inplace modify a file in-place\n"
43 " -c --case use case sensitivity\n"
44 " -e --explain explain the matches\n"
45 " -f --format fancy|plain|bare|file:line set the output format\n"
46 " -g --grammar <grammar-file> use the specified file as a grammar\n"
47 " -h --help print the usage and quit\n"
48 " -i --ignore-case preform matching case-insensitively\n"
49 " -l --list-files list filenames only\n"
50 " -r --replace <replacement> replace the input pattern with the given replacement\n"
51 " -s --skip <skip-pattern> skip over the given pattern when looking for matches\n"
52 " -v --verbose print verbose debugging info\n"
53 " -w --word <string-pat> find words matching the given string pattern\n");
55 // Used as a heuristic to check if a file is binary or text:
56 #define CHECK_FIRST_N_BYTES 256
58 #define USE_DEFAULT_CONTEXT -3
59 #define ALL_CONTEXT -2
62 // Flag-configurable options:
64 int context_before, context_after;
65 bool ignorecase, verbose, git_mode, print_filenames;
66 enum { MODE_NORMAL, MODE_LISTFILES, MODE_INPLACE, MODE_EXPLAIN } mode;
67 enum { FORMAT_AUTO, FORMAT_FANCY, FORMAT_PLAIN, FORMAT_BARE, FORMAT_FILE_LINE } format;
70 .context_before = USE_DEFAULT_CONTEXT,
71 .context_after = USE_DEFAULT_CONTEXT,
73 .print_filenames = true,
76 .format = FORMAT_AUTO,
80 const char *LINE_FORMATS[] = {
81 [FORMAT_FANCY] = "\033[0;2m#\033(0\x78\033(B",
82 [FORMAT_PLAIN] = "#|",
84 [FORMAT_FILE_LINE] = "@:#0:",
87 // If a file is partly through being modified when the program exits, restore it from backup.
88 static FILE *modifying_file = NULL;
89 static file_t *backup_file;
92 // Helper function to reduce code duplication
94 static inline void fprint_filename(FILE *out, const char *filename) {
95 if (!filename[0]) return;
96 if (options.format == FORMAT_FANCY) fprintf(out, "\033[0;1;4;33m%s\033[m\n", filename);
97 else fprintf(out, "%s:\n", filename);
100 static void *portable_memrchr(const void *s, int c, size_t n) {
101 const unsigned char *p = (const unsigned char *)s + n;
103 if (*--p == (unsigned char)c) return (void *)p;
109 // If there was a parse error while building a pattern, print an error message and exit.
111 static inline bp_pat_t *assert_pat(const char *start, const char *end, maybe_pat_t maybe_pat) {
112 if (!end) end = start + strlen(start);
113 if (!maybe_pat.success) {
114 const char *err_start = maybe_pat.value.error.start, *err_end = maybe_pat.value.error.end,
115 *err_msg = maybe_pat.value.error.msg;
117 const char *nl = portable_memrchr(start, '\n', (size_t)(err_start - start));
118 const char *sol = nl ? nl + 1 : start;
119 nl = memchr(err_start, '\n', (size_t)(end - err_start));
120 const char *eol = nl ? nl : end;
121 if (eol < err_end) err_end = eol;
123 fprintf(stderr, "\033[31;1m%s\033[0m\n", err_msg);
124 fprintf(stderr, "%.*s\033[41;30m%.*s\033[m%.*s\n", (int)(err_start - sol), sol, (int)(err_end - err_start),
125 err_start, (int)(eol - err_end), err_end);
126 fprintf(stderr, "\033[34;1m");
128 for (; p < err_start; ++p)
129 (void)fputc(*p == '\t' ? '\t' : ' ', stderr);
130 if (err_start == err_end) ++err_end;
131 for (; p < err_end; ++p)
133 // Some janky hacks: 8 ^'s, backtrack 8 spaces, move forward a tab stop, clear any ^'s that overshot
134 fprintf(stderr, "^^^^^^^^\033[8D\033[I\033[K");
135 else (void)fputc('^', stderr);
136 fprintf(stderr, "\033[m\n");
139 return maybe_pat.value.pat;
143 // Look for a key/value flag at the first position in the given argument list.
144 // If the flag is found, update `next` to point to the next place to check for a flag.
145 // The contents of argv[0] may be modified for single-char flags.
146 // Return the flag's value.
148 __attribute__((nonnull)) static char *get_flag(char *argv[], const char *flag, char ***next) {
149 size_t n = strlen(flag);
150 if (strncmp(argv[0], flag, n) != 0) return NULL;
151 if (argv[0][n] == '=') { // --foo=baz, -f=baz
153 return &argv[0][n + 1];
154 } else if (argv[0][n] == '\0') { // --foo baz, -f baz
155 if (!argv[1]) errx(EXIT_FAILURE, "Expected argument after '%s'\n\n%s", flag, usage);
158 } else if (flag[0] == '-' && flag[1] != '-' && flag[2] == '\0') { // -f...
166 // Look for a flag at the first position in the given argument list.
167 // If the flag is found, update `next` to point to the next place to check for a flag.
168 // The contents of argv[0] may be modified for single-char flags.
169 // Return a boolean for whether or not the flag was found.
171 __attribute__((nonnull)) static bool get_boolflag(char *argv[], const char *flag, char ***next) {
172 size_t n = strlen(flag);
173 if (strncmp(argv[0], flag, n) != 0) return false;
174 if (argv[0][n] == '\0') { // --foo, -f
177 } else if (flag[0] == '-' && flag[1] != '-' && flag[2] == '\0') { // -f...
178 memmove(&argv[0][1], &argv[0][2], 1 + strlen(&argv[0][2])); // Shift the flags down
186 // Scan the first few dozen bytes of a file and return 1 if the contents all
187 // look like printable text characters, otherwise return 0.
189 static int is_text_file(const char *filename) {
190 int fd = open(filename, O_RDONLY);
191 if (fd < 0) return 0;
192 char buf[CHECK_FIRST_N_BYTES];
193 ssize_t len = read(fd, buf, sizeof(buf) / sizeof(char));
195 if (len < 0) return 0;
196 for (ssize_t i = 0; i < len; i++)
197 if (isascii(buf[i]) && !(isprint(buf[i]) || isspace(buf[i]))) return 0;
202 // Print matches in a visual explanation style
204 static int explain_matches(file_t *f, bp_pat_t *pattern, bp_pat_t *defs) {
206 for (bp_match_t *m = NULL; next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase);) {
207 if (++nmatches == 1) {
208 if (options.print_filenames) fprint_filename(stdout, f->filename);
209 } else printf("\n\n");
216 // Cleanup function to ensure no temp files are left around if the program
217 // exits unexpectedly.
219 static void cleanup(void) {
220 if (modifying_file && backup_file) {
221 rewind(modifying_file);
222 ftruncate(fileno(modifying_file), 0);
223 (void)fwrite(backup_file->start, 1, (size_t)(backup_file->end - backup_file->start), modifying_file);
224 fclose(modifying_file);
225 modifying_file = NULL;
227 if (backup_file) destroy_file(&backup_file);
231 // Signal handler to ensure cleanup happens.
233 static void sig_handler(int sig) {
235 if (kill(0, sig)) _exit(EXIT_FAILURE);
238 int fprint_linenum(FILE *out, file_t *f, int linenum, const char *normal_color) {
240 switch (options.format) {
244 for (int i = (int)f->nlines; i > 0; i /= 10)
246 if (options.format == FORMAT_FANCY)
247 printed += fprintf(out, "\033[0;2m%*d\033(0\x78\033(B%s", space, linenum, normal_color ? normal_color : "");
248 else printed += fprintf(out, "%*d|", space, linenum);
251 case FORMAT_FILE_LINE: {
252 printed += fprintf(out, "%s:%d:", f->filename, linenum);
260 static file_t *printing_file = NULL;
261 static int last_line_num = -1;
262 static int _fprint_between(FILE *out, const char *start, const char *end, const char *normal_color) {
265 // Cheeky lookbehind to see if line number should be printed
266 if (start == printing_file->start || start[-1] == '\n') {
267 int linenum = (int)get_line_number(printing_file, start);
268 if (last_line_num != linenum) {
269 printed += fprint_linenum(out, printing_file, linenum, normal_color);
270 last_line_num = linenum;
273 const char *line_end = memchr(start, '\n', (size_t)(end - start));
274 if (line_end && line_end != end) {
275 printed += fwrite(start, sizeof(char), (size_t)(line_end - start + 1), out);
276 start = line_end + 1;
278 if (end > start) printed += fwrite(start, sizeof(char), (size_t)(end - start), out);
281 } while (start < end);
285 static void fprint_context(FILE *out, file_t *f, const char *prev, const char *next) {
286 if (options.context_before == ALL_CONTEXT || options.context_after == ALL_CONTEXT) {
287 _fprint_between(out, prev ? prev : f->start, next ? next : f->end, "\033[m");
290 const char *before_next = next;
291 if (next && options.context_before >= 0) {
292 size_t line_before_next = get_line_number(printing_file, next);
294 options.context_before >= (int)line_before_next ? 1 : line_before_next - (size_t)options.context_before;
295 before_next = get_line(printing_file, line_before_next);
296 if (prev && before_next < prev) before_next = prev;
298 const char *after_prev = prev;
299 if (prev && options.context_after >= 0) {
300 size_t line_after_prev = get_line_number(printing_file, prev) + (size_t)options.context_after + 1;
301 after_prev = line_after_prev > printing_file->nlines
303 : get_line(printing_file,
304 line_after_prev > printing_file->nlines ? printing_file->nlines : line_after_prev);
305 if (next && after_prev > next) after_prev = next;
307 if (next && prev && after_prev >= before_next) {
308 _fprint_between(out, prev, next, "\033[m");
310 if (prev) _fprint_between(out, prev, after_prev, "\033[m");
311 if (next) _fprint_between(out, before_next, next, "\033[m");
315 static void on_nl(FILE *out) {
316 switch (options.format) {
319 for (int i = (int)printing_file->nlines; i > 0; i /= 10)
321 fprintf(out, "%s", options.format == FORMAT_FANCY ? "\033[0;2m\033(0\x78\033(B\033[m" : "|");
328 // Print all the matches in a file.
330 static int print_matches(FILE *out, file_t *f, bp_pat_t *pattern, bp_pat_t *defs) {
331 static int printed_filenames = 0;
333 const char *prev = NULL;
338 print_options_t print_opts = {.fprint_between = _fprint_between, .on_nl = on_nl};
339 if (options.format == FORMAT_FANCY) {
340 print_opts.match_color = "\033[0;31;1m";
341 print_opts.replace_color = "\033[0;34;1m";
342 print_opts.normal_color = "\033[m";
344 for (bp_match_t *m = NULL; next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase);) {
345 if (++matches == 1 && options.print_filenames) {
346 if (printed_filenames++ > 0) printf("\n");
347 fprint_filename(out, f->filename);
349 fprint_context(out, f, prev, m->start);
350 if (print_opts.normal_color) fprintf(out, "%s", print_opts.normal_color);
351 fprint_match(out, f->start, m, &print_opts);
352 if (print_opts.normal_color) fprintf(out, "%s", print_opts.normal_color);
355 // Print trailing context if needed:
357 fprint_context(out, f, prev, NULL);
358 if (last_line_num < 0) { // Hacky fix to ensure line number gets printed for `bp '{$$}'`
359 fprint_linenum(out, f, f->nlines, print_opts.normal_color);
364 printing_file = NULL;
370 // For a given filename, open the file and attempt to match the given pattern
371 // against it, printing any results according to the flags.
373 __attribute__((nonnull)) static int process_file(const char *filename, bp_pat_t *pattern, bp_pat_t *defs) {
374 file_t *f = load_file(NULL, filename);
376 fprintf(stderr, "Could not open file: %s\n%s\n", filename, strerror(errno));
381 if (options.mode == MODE_EXPLAIN) {
382 matches += explain_matches(f, pattern, defs);
383 } else if (options.mode == MODE_LISTFILES) {
384 bp_match_t *m = NULL;
385 if (next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase)) {
386 printf("%s\n", f->filename);
390 } else if (options.mode == MODE_INPLACE) {
391 bp_match_t *m = NULL;
392 bool found = next_match(&m, f->start, f->end, pattern, defs, options.skip, options.ignorecase);
394 if (!found) return 0;
396 // Ensure the file is resident in memory:
398 file_t *copy = spoof_file(NULL, f->filename, f->start, (ssize_t)(f->end - f->start));
402 FILE *out = fopen(filename, "w");
403 // Set these temporary values in case the program crashes while in the
404 // middle of inplace modifying a file. If that happens, these variables
405 // are used to restore the original file contents.
406 modifying_file = out;
409 matches += print_matches(out, f, pattern, defs);
411 modifying_file = NULL;
415 printf(getenv("NO_COLOR") ? "%s: %d replacement%s\n" : "\x1b[33;1m%s:\x1b[m %d replacement%s\n", filename,
416 matches, matches == 1 ? "" : "s");
418 matches += print_matches(stdout, f, pattern, defs);
422 if (recycle_all_matches() != 0)
423 fprintf(stderr, "\033[33;1mMemory leak: there should no longer be any matches in use at this point.\033[m\n");
425 (void)fflush(stdout);
430 // Recursively process all non-dotfile files in the given directory.
432 __attribute__((nonnull)) static int process_dir(const char *dirname, bp_pat_t *pattern, bp_pat_t *defs) {
435 char globpath[PATH_MAX + 1] = {'\0'};
436 if (snprintf(globpath, PATH_MAX, "%s/*", dirname) > (int)PATH_MAX)
437 errx(EXIT_FAILURE, "Filename is too long: %s/*", dirname);
438 int status = glob(globpath, 0, NULL, &globbuf);
439 if (status == GLOB_ABORTED || status == GLOB_NOSPACE)
440 errx(EXIT_FAILURE, "Failed to get directory contents: %s", dirname);
441 if (status != GLOB_NOMATCH) {
443 for (size_t i = 0; i < globbuf.gl_pathc; i++) {
444 if (lstat(globbuf.gl_pathv[i], &statbuf) != 0) continue;
445 if (S_ISLNK(statbuf.st_mode)) continue; // Skip symbolic links
446 else if (S_ISDIR(statbuf.st_mode)) matches += process_dir(globbuf.gl_pathv[i], pattern, defs);
447 else if (is_text_file(globbuf.gl_pathv[i])) matches += process_file(globbuf.gl_pathv[i], pattern, defs);
455 // Process git files using `git ls-files ...`
457 __attribute__((nonnull(1))) static int process_git_files(bp_pat_t *pattern, bp_pat_t *defs, int argc, char *argv[]) {
459 require(pipe(fds), "Failed to create pipe");
460 pid_t child = require(fork(), "Failed to fork");
462 const char **git_args = new (char * [3 + argc + 1]);
464 git_args[g++] = "git";
465 git_args[g++] = "ls-files";
466 git_args[g++] = "-z";
468 git_args[g++] = *(argv++);
469 require(dup2(fds[STDOUT_FILENO], STDOUT_FILENO), "Failed to hook up pipe to stdout");
470 require(close(fds[STDIN_FILENO]), "Failed to close read end of pipe");
471 (void)execvp("git", (char **)git_args);
474 require(close(fds[STDOUT_FILENO]), "Failed to close write end of pipe");
475 FILE *fp = require(fdopen(fds[STDIN_FILENO], "r"), "Could not open pipe file descriptor");
477 size_t path_size = 0;
479 while (getdelim(&path, &path_size, '\0', fp) > 0)
480 found += process_file(path, pattern, defs);
481 if (path) delete (&path);
482 require(fclose(fp), "Failed to close read end of pipe");
484 while (waitpid(child, &status, 0) != child)
486 if (!((WIFEXITED(status) == 1) && (WEXITSTATUS(status) == 0))) errx(EXIT_FAILURE, "`git ls-files -z` failed.");
491 // Load the given grammar (semicolon-separated definitions)
492 // and return the first rule defined.
494 static bp_pat_t *load_grammar(bp_pat_t *defs, file_t *f) {
495 return chain_together(defs, assert_pat(f->start, f->end, bp_pattern(f->start, f->end)));
499 // Convert a context string to an integer
501 static int context_from_flag(const char *flag) {
502 if (streq(flag, "all")) return ALL_CONTEXT;
503 if (streq(flag, "none")) return NO_CONTEXT;
504 return (int)strtol(flag, NULL, 10);
508 // Check if any letters are uppercase
510 static bool any_uppercase(const char *str) {
511 for (; *str; ++str) {
512 if (isupper(*str)) return true;
517 #define FLAG(f) (flag = get_flag(argv, f, &argv))
518 #define BOOLFLAG(f) get_boolflag(argv, f, &argv)
520 int main(int argc, char *argv[]) {
523 bp_pat_t *defs = NULL;
524 file_t *loaded_files = NULL;
525 bp_pat_t *pattern = NULL;
528 file_t *builtins_file = load_file(&loaded_files, BP_PREFIX "/" BP_NAME "/builtins.bp");
529 if (builtins_file) defs = load_grammar(defs, builtins_file);
530 file_t *local_file = load_file(&loaded_files, BP_PREFIX "/share/" BP_NAME "/grammars/builtins.bp");
531 if (local_file) defs = load_grammar(defs, local_file);
533 bool explicit_case_sensitivity = false;
535 ++argv; // skip program name
537 if (streq(argv[0], "--")) {
540 } else if (BOOLFLAG("-h") || BOOLFLAG("--help")) {
541 printf("%s\n\n%s", description, usage);
543 } else if (BOOLFLAG("-v") || BOOLFLAG("--verbose")) {
544 options.verbose = true;
545 } else if (BOOLFLAG("-e") || BOOLFLAG("--explain")) {
546 options.mode = MODE_EXPLAIN;
547 } else if (BOOLFLAG("-I") || BOOLFLAG("--inplace")) {
548 options.mode = MODE_INPLACE;
549 options.print_filenames = false;
550 options.format = FORMAT_BARE;
551 } else if (BOOLFLAG("-G") || BOOLFLAG("--git")) {
552 options.git_mode = true;
553 } else if (BOOLFLAG("-i") || BOOLFLAG("--ignore-case")) {
554 options.ignorecase = true;
555 explicit_case_sensitivity = true;
556 } else if (BOOLFLAG("-c") || BOOLFLAG("--case")) {
557 options.ignorecase = false;
558 explicit_case_sensitivity = true;
559 } else if (BOOLFLAG("-l") || BOOLFLAG("--list-files")) {
560 options.mode = MODE_LISTFILES;
561 } else if (FLAG("-r") || FLAG("--replace")) {
562 if (!pattern) errx(EXIT_FAILURE, "No pattern has been defined for replacement to operate on");
563 // TODO: spoof file as sprintf("pattern => '%s'", flag)
564 // except that would require handling edge cases like quotation marks etc.
565 pattern = assert_pat(flag, NULL, bp_replacement(pattern, flag, flag + strlen(flag)));
566 if (options.context_before == USE_DEFAULT_CONTEXT) options.context_before = ALL_CONTEXT;
567 if (options.context_after == USE_DEFAULT_CONTEXT) options.context_after = ALL_CONTEXT;
568 } else if (FLAG("-g") || FLAG("--grammar")) {
570 if (strlen(flag) > 3 && strncmp(&flag[strlen(flag) - 3], ".bp", 3) == 0) f = load_file(&loaded_files, flag);
571 if (f == NULL) f = load_filef(&loaded_files, "%s/.config/" BP_NAME "/%s.bp", getenv("HOME"), flag);
572 if (f == NULL) f = load_filef(&loaded_files, BP_PREFIX "/share/" BP_NAME "/grammars/%s.bp", flag);
573 if (f == NULL) errx(EXIT_FAILURE, "Couldn't find grammar: %s", flag);
574 defs = load_grammar(defs, f); // Keep in memory for debug output
575 } else if (FLAG("-w") || FLAG("--word")) {
576 require(asprintf(&flag, "{|}%s{|}", flag), "Could not allocate memory");
577 file_t *arg_file = spoof_file(&loaded_files, "<word pattern>", flag, -1);
578 if (!explicit_case_sensitivity) options.ignorecase = !any_uppercase(flag);
580 bp_pat_t *p = assert_pat(arg_file->start, arg_file->end, bp_stringpattern(arg_file->start, arg_file->end));
581 pattern = chain_together(pattern, p);
582 } else if (FLAG("-s") || FLAG("--skip")) {
583 bp_pat_t *s = assert_pat(flag, NULL, bp_pattern(flag, flag + strlen(flag)));
584 options.skip = either_pat(options.skip, s);
585 } else if (FLAG("-C") || FLAG("--context")) {
586 options.context_before = options.context_after = context_from_flag(flag);
587 } else if (FLAG("-B") || FLAG("--before-context")) {
588 options.context_before = context_from_flag(flag);
589 } else if (FLAG("-A") || FLAG("--after-context")) {
590 options.context_after = context_from_flag(flag);
591 } else if (FLAG("-f") || FLAG("--format")) {
592 if (streq(flag, "fancy")) options.format = FORMAT_FANCY;
593 else if (streq(flag, "plain")) options.format = FORMAT_PLAIN;
594 else if (streq(flag, "bare")) options.format = FORMAT_BARE;
595 else if (streq(flag, "file:line")) {
596 options.format = FORMAT_FILE_LINE;
597 options.print_filenames = 0;
598 } else if (!streq(flag, "auto")) errx(EXIT_FAILURE, "Unknown --format option: %s", flag);
599 } else if (argv[0][0] != '-' || strncmp(argv[0], "->", 2) == 0) {
600 // As a special case, support `bp '->foo'` as a way to search for
601 // pointer field accesses without needing to escape anything.
602 if (pattern != NULL) break;
603 bp_pat_t *p = assert_pat(argv[0], NULL, bp_stringpattern(argv[0], argv[0] + strlen(argv[0])));
604 if (!explicit_case_sensitivity) options.ignorecase = !any_uppercase(argv[0]);
605 pattern = chain_together(pattern, p);
608 errx(EXIT_FAILURE, "Unrecognized flag: %s\n\n%s", argv[0], usage);
612 if (pattern == NULL) errx(EXIT_FAILURE, "No pattern provided.\n\n%s", usage);
614 for (argc = 0; argv[argc]; ++argc)
617 if (options.context_before == USE_DEFAULT_CONTEXT) options.context_before = 0;
618 if (options.context_after == USE_DEFAULT_CONTEXT) options.context_after = 0;
620 if (options.format == FORMAT_AUTO)
621 options.format = isatty(STDOUT_FILENO) ? (getenv("NO_COLOR") ? FORMAT_PLAIN : FORMAT_FANCY) : FORMAT_BARE;
623 // If any of these signals triggers, and there is a temporary file in use,
624 // be sure to clean it up before exiting.
625 int signals[] = {SIGTERM, SIGINT, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGSEGV, SIGTSTP};
626 struct sigaction sa = {.sa_handler = &sig_handler, .sa_flags = (int)(SA_NODEFER | SA_RESETHAND)};
627 for (size_t i = 0; i < sizeof(signals) / sizeof(signals[0]); i++)
628 require(sigaction(signals[i], &sa, NULL), "Failed to set signal handler");
630 // Handle exit() calls gracefully:
631 require(atexit(&cleanup), "Failed to set cleanup handler at exit");
633 if (options.verbose) {
634 fputs("Matching pattern: ", stderr);
635 fprint_pattern(stderr, pattern);
639 // Default to git mode if there's a .git directory and no files were specified:
641 if (argc == 0 && stat(".git", &gitdir) == 0 && S_ISDIR(gitdir.st_mode)) options.git_mode = true;
644 if (!isatty(STDIN_FILENO) && !argv[0]) {
646 options.print_filenames = false; // Don't print filename on stdin
647 found += process_file("", pattern, defs);
648 } else if (options.git_mode) {
649 // Get the list of files from `git --ls-files ...`
650 found = process_git_files(pattern, defs, argc, argv);
651 } else if (argv[0]) {
652 // Files passed in as command line args:
655 && !(stat(argv[0], &statbuf) == 0
656 && S_ISDIR(statbuf.st_mode))) // Don't print filename for single-file matching
657 options.print_filenames = false;
658 for (; argv[0]; argv++) {
659 if (stat(argv[0], &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) // Symlinks are okay if manually specified
660 found += process_dir(argv[0], pattern, defs);
661 else found += process_file(argv[0], pattern, defs);
664 // No files, no piped in input, so use files in current dir, recursively
665 found += process_dir(".", pattern, defs);
668 // This code frees up all residual heap-allocated memory. Since the program
669 // is about to exit, this step is unnecessary. However, it is useful for
670 // tracking down memory leaks.
673 while (loaded_files) {
674 file_t *next = loaded_files->next;
675 destroy_file(&loaded_files);
679 exit(found > 0 ? EXIT_SUCCESS : EXIT_FAILURE);
682 // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0