2021-01-12 21:04:43 -08:00
|
|
|
//
|
|
|
|
// bp.c - Source code for the bp parser
|
|
|
|
//
|
|
|
|
// See `man ./bp.1` for more details
|
|
|
|
//
|
2021-01-18 11:15:53 -08:00
|
|
|
|
2021-05-18 10:10:35 -07:00
|
|
|
#include <ctype.h>
|
2021-01-26 17:54:23 -08:00
|
|
|
#include <err.h>
|
2021-01-17 18:06:00 -08:00
|
|
|
#include <errno.h>
|
2020-09-11 01:28:06 -07:00
|
|
|
#include <fcntl.h>
|
2020-09-13 15:55:09 -07:00
|
|
|
#include <glob.h>
|
2020-09-12 15:49:51 -07:00
|
|
|
#include <limits.h>
|
2021-01-15 18:23:18 -08:00
|
|
|
#include <signal.h>
|
2021-01-18 10:30:17 -08:00
|
|
|
#include <stdbool.h>
|
2020-09-11 01:28:06 -07:00
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
2021-01-17 17:30:31 -08:00
|
|
|
#include <sys/stat.h>
|
2021-01-17 09:21:58 -08:00
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/wait.h>
|
2020-09-11 01:28:06 -07:00
|
|
|
#include <unistd.h>
|
|
|
|
|
2021-01-15 19:35:39 -08:00
|
|
|
#include "definitions.h"
|
2021-08-01 21:30:41 -07:00
|
|
|
#include "explain.h"
|
2021-01-15 19:44:16 -08:00
|
|
|
#include "files.h"
|
2020-12-14 22:20:16 -08:00
|
|
|
#include "json.h"
|
2021-01-15 19:44:16 -08:00
|
|
|
#include "match.h"
|
|
|
|
#include "pattern.h"
|
2021-01-15 19:27:25 -08:00
|
|
|
#include "print.h"
|
2020-09-11 01:28:06 -07:00
|
|
|
#include "utils.h"
|
|
|
|
|
2021-01-15 18:32:56 -08:00
|
|
|
#ifndef BP_NAME
|
|
|
|
#define BP_NAME "bp"
|
|
|
|
#endif
|
|
|
|
|
2021-03-26 00:06:11 -07:00
|
|
|
static const char *description = (
|
|
|
|
BP_NAME" - a Parsing Expression Grammar command line tool");
|
2020-09-11 01:28:06 -07:00
|
|
|
static const char *usage = (
|
|
|
|
"Usage:\n"
|
2021-05-19 21:58:54 -07:00
|
|
|
" "BP_NAME" [flags] <pattern> [<files>...]\n\n"
|
2020-09-11 01:28:06 -07:00
|
|
|
"Flags:\n"
|
2020-09-14 12:39:31 -07:00
|
|
|
" -h --help print the usage and quit\n"
|
|
|
|
" -v --verbose print verbose debugging info\n"
|
2020-12-12 16:31:53 -08:00
|
|
|
" -e --explain explain the matches\n"
|
2020-12-14 22:32:47 -08:00
|
|
|
" -j --json print matches as a list of JSON objects\n"
|
2020-09-14 12:39:31 -07:00
|
|
|
" -i --ignore-case preform matching case-insensitively\n"
|
2021-01-15 18:23:18 -08:00
|
|
|
" -I --inplace modify a file in-place\n"
|
2020-12-17 16:23:29 -08:00
|
|
|
" -l --list-files list filenames only\n"
|
2020-12-12 16:31:53 -08:00
|
|
|
" -p --pattern <pat> provide a pattern (equivalent to bp '\\(<pat>)')\n"
|
2021-07-30 20:46:50 -07:00
|
|
|
" -w --word <string-pat> find words matching the given string pattern\n"
|
2020-09-14 12:39:31 -07:00
|
|
|
" -r --replace <replacement> replace the input pattern with the given replacement\n"
|
2021-05-19 21:58:54 -07:00
|
|
|
" -s --skip <skip-pattern> skip over the given pattern when looking for matches\n"
|
2021-08-02 12:25:52 -07:00
|
|
|
" -B --context-before <n> set number of lines of context to print before the match\n"
|
|
|
|
" -B --context-after <n> set number of lines of context to print after the match\n"
|
|
|
|
" -C --context <context> set number of lines of context to print before and after the match\n"
|
2021-09-02 18:07:18 -07:00
|
|
|
" -f --format fancy|plain|bare|file:line set the output format\n"
|
2021-05-19 21:58:54 -07:00
|
|
|
" -g --grammar <grammar-file> use the specified file as a grammar");
|
2020-09-07 23:05:38 -07:00
|
|
|
|
2021-01-18 12:57:35 -08:00
|
|
|
// Used as a heuristic to check if a file is binary or text:
|
2021-07-26 20:59:45 -07:00
|
|
|
#define CHECK_FIRST_N_BYTES 256
|
2021-01-18 12:57:35 -08:00
|
|
|
|
2021-01-15 02:05:17 -08:00
|
|
|
// Flag-configurable options:
|
2021-05-12 20:33:27 -07:00
|
|
|
static struct {
|
2021-08-02 12:25:52 -07:00
|
|
|
int context_before, context_after;
|
2021-08-06 13:15:27 -07:00
|
|
|
bool ignorecase, verbose, git_mode, print_filenames;
|
2021-05-12 20:33:27 -07:00
|
|
|
enum { MODE_NORMAL, MODE_LISTFILES, MODE_INPLACE, MODE_JSON, MODE_EXPLAIN } mode;
|
2021-09-02 18:07:18 -07:00
|
|
|
enum { FORMAT_AUTO, FORMAT_FANCY, FORMAT_PLAIN, FORMAT_BARE, FORMAT_FILE_LINE } format;
|
2021-05-12 20:33:27 -07:00
|
|
|
pat_t *skip;
|
|
|
|
} options = {
|
2021-08-02 12:25:52 -07:00
|
|
|
.context_before = USE_DEFAULT_CONTEXT,
|
|
|
|
.context_after = USE_DEFAULT_CONTEXT,
|
2021-05-12 20:33:27 -07:00
|
|
|
.ignorecase = false,
|
2021-08-06 13:15:27 -07:00
|
|
|
.print_filenames = true,
|
2021-05-12 20:33:27 -07:00
|
|
|
.verbose = false,
|
|
|
|
.mode = MODE_NORMAL,
|
|
|
|
.format = FORMAT_AUTO,
|
|
|
|
.skip = NULL,
|
|
|
|
};
|
2020-12-12 16:31:53 -08:00
|
|
|
|
2021-09-02 18:07:18 -07:00
|
|
|
const char *LINE_FORMATS[] = {
|
|
|
|
[FORMAT_FANCY] = "\033[0;2m#\033(0\x78\033(B",
|
|
|
|
[FORMAT_PLAIN] = "#|",
|
|
|
|
[FORMAT_BARE] = "",
|
|
|
|
[FORMAT_FILE_LINE] = "@:#0:",
|
|
|
|
};
|
|
|
|
|
2021-03-03 17:24:23 -08:00
|
|
|
// If a file is partly through being modified when the program exits, restore it from backup.
|
|
|
|
static FILE *modifying_file = NULL;
|
|
|
|
static file_t *backup_file;
|
2021-01-15 18:23:18 -08:00
|
|
|
|
|
|
|
//
|
|
|
|
// Helper function to reduce code duplication
|
|
|
|
//
|
|
|
|
static inline void fprint_filename(FILE *out, const char *filename)
|
|
|
|
{
|
|
|
|
if (!filename[0]) return;
|
2021-05-12 20:33:27 -07:00
|
|
|
if (options.format == FORMAT_FANCY) fprintf(out, "\033[0;1;4;33m%s\033[0m\n", filename);
|
2021-01-15 18:23:18 -08:00
|
|
|
else fprintf(out, "%s:\n", filename);
|
|
|
|
}
|
2021-01-12 22:33:28 -08:00
|
|
|
|
2021-01-12 22:22:38 -08:00
|
|
|
//
|
2021-02-07 11:33:17 -08:00
|
|
|
// 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.
|
2021-01-12 22:22:38 -08:00
|
|
|
//
|
2021-01-15 18:23:18 -08:00
|
|
|
__attribute__((nonnull))
|
2021-02-07 11:33:17 -08:00
|
|
|
static char *get_flag(char *argv[], const char *flag, char ***next)
|
2020-09-11 02:55:15 -07:00
|
|
|
{
|
|
|
|
size_t n = strlen(flag);
|
2021-02-07 11:33:17 -08:00
|
|
|
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];
|
2020-09-11 02:55:15 -07:00
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
2020-09-13 15:55:09 -07:00
|
|
|
|
2021-01-17 10:27:33 -08:00
|
|
|
//
|
2021-02-07 11:33:17 -08:00
|
|
|
// 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.
|
2021-01-17 10:27:33 -08:00
|
|
|
//
|
|
|
|
__attribute__((nonnull))
|
2021-02-07 11:33:17 -08:00
|
|
|
static bool get_boolflag(char *argv[], const char *flag, char ***next)
|
2021-01-17 10:27:33 -08:00
|
|
|
{
|
2021-02-07 11:33:17 -08:00
|
|
|
size_t 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;
|
2021-01-17 10:27:33 -08:00
|
|
|
}
|
2021-02-07 11:33:17 -08:00
|
|
|
return false;
|
2021-01-17 10:27:33 -08:00
|
|
|
}
|
|
|
|
|
2021-01-15 01:19:10 -08:00
|
|
|
//
|
|
|
|
// 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 is_text_file(const char *filename)
|
|
|
|
{
|
|
|
|
int fd = open(filename, O_RDONLY);
|
|
|
|
if (fd < 0) return 0;
|
2021-01-18 12:57:35 -08:00
|
|
|
char buf[CHECK_FIRST_N_BYTES];
|
2021-01-18 11:28:39 -08:00
|
|
|
ssize_t len = read(fd, buf, sizeof(buf)/sizeof(char));
|
2021-01-15 01:19:10 -08:00
|
|
|
(void)close(fd);
|
2021-05-18 10:10:35 -07:00
|
|
|
if (len < 0) return 0;
|
|
|
|
for (ssize_t i = 0; i < len; i++)
|
2021-05-19 22:16:39 -07:00
|
|
|
if (isascii(buf[i]) && !(isprint(buf[i]) || isspace(buf[i])))
|
|
|
|
return 0;
|
2021-01-15 01:19:10 -08:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Print matches in JSON format.
|
|
|
|
//
|
2021-01-15 18:38:06 -08:00
|
|
|
static int print_matches_as_json(def_t *defs, file_t *f, pat_t *pattern)
|
2021-01-15 01:19:10 -08:00
|
|
|
{
|
2021-05-20 01:00:10 -07:00
|
|
|
static int matches = 0;
|
2021-07-26 20:59:45 -07:00
|
|
|
match_t *m = NULL;
|
|
|
|
while ((m = next_match(defs, f, m, pattern, options.skip, options.ignorecase))) {
|
2021-01-15 01:19:10 -08:00
|
|
|
if (++matches > 1)
|
|
|
|
printf(",\n");
|
2021-05-20 18:31:28 -07:00
|
|
|
printf("{\"filename\":\"%s\",\"match\":", f->filename);
|
|
|
|
json_match(f->start, m, options.verbose);
|
|
|
|
printf("}");
|
2021-01-15 01:19:10 -08:00
|
|
|
}
|
2021-07-26 20:59:45 -07:00
|
|
|
if (m) recycle_if_unused(&m);
|
2021-01-15 01:19:10 -08:00
|
|
|
return matches;
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Print matches in a visual explanation style
|
|
|
|
//
|
2021-01-15 18:38:06 -08:00
|
|
|
static int explain_matches(def_t *defs, file_t *f, pat_t *pattern)
|
2021-01-15 01:19:10 -08:00
|
|
|
{
|
|
|
|
int matches = 0;
|
2021-07-26 20:59:45 -07:00
|
|
|
match_t *m = NULL;
|
|
|
|
while ((m = next_match(defs, f, m, pattern, options.skip, options.ignorecase))) {
|
2021-08-06 13:15:27 -07:00
|
|
|
if (++matches == 1) {
|
|
|
|
if (options.print_filenames)
|
|
|
|
fprint_filename(stdout, f->filename);
|
|
|
|
} else
|
2021-01-15 01:19:10 -08:00
|
|
|
printf("\n\n");
|
2021-08-01 21:30:41 -07:00
|
|
|
explain_match(m);
|
2021-01-15 01:19:10 -08:00
|
|
|
}
|
2021-07-26 20:59:45 -07:00
|
|
|
if (m) recycle_if_unused(&m);
|
2021-01-15 01:19:10 -08:00
|
|
|
return matches;
|
|
|
|
}
|
|
|
|
|
2021-01-15 18:23:18 -08:00
|
|
|
//
|
|
|
|
// Cleanup function to ensure no temp files are left around if the program
|
|
|
|
// exits unexpectedly.
|
|
|
|
//
|
|
|
|
static void cleanup(void)
|
|
|
|
{
|
2021-03-03 17:24:23 -08:00
|
|
|
if (modifying_file && backup_file) {
|
|
|
|
rewind(modifying_file);
|
|
|
|
ftruncate(fileno(modifying_file), 0);
|
2021-05-22 22:02:22 -07:00
|
|
|
(void)fwrite(backup_file->start, 1,
|
|
|
|
(size_t)(backup_file->end - backup_file->start),
|
|
|
|
modifying_file);
|
2021-03-03 17:24:23 -08:00
|
|
|
fclose(modifying_file);
|
|
|
|
modifying_file = NULL;
|
2021-01-15 18:23:18 -08:00
|
|
|
}
|
2021-03-03 17:24:23 -08:00
|
|
|
if (backup_file) destroy_file(&backup_file);
|
2021-01-15 18:23:18 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Signal handler to ensure cleanup happens.
|
|
|
|
//
|
2021-01-18 09:05:35 -08:00
|
|
|
static void sig_handler(int sig)
|
|
|
|
{
|
|
|
|
cleanup();
|
2021-01-18 10:30:17 -08:00
|
|
|
if (kill(0, sig)) _exit(EXIT_FAILURE);
|
2021-01-18 09:05:35 -08:00
|
|
|
}
|
2021-01-15 18:23:18 -08:00
|
|
|
|
2021-01-15 01:19:10 -08:00
|
|
|
//
|
|
|
|
// Print all the matches in a file.
|
|
|
|
//
|
2021-08-06 13:15:27 -07:00
|
|
|
static int print_matches(FILE *out, def_t *defs, file_t *f, pat_t *pattern)
|
2021-01-15 01:19:10 -08:00
|
|
|
{
|
|
|
|
static int printed_filenames = 0;
|
|
|
|
int matches = 0;
|
|
|
|
printer_t pr = {
|
|
|
|
.file = f,
|
2021-08-02 12:25:52 -07:00
|
|
|
.context_before = options.context_before,
|
|
|
|
.context_after = options.context_after,
|
2021-05-12 20:33:27 -07:00
|
|
|
.use_color = options.format == FORMAT_FANCY,
|
2021-09-02 18:07:18 -07:00
|
|
|
.lineformat = LINE_FORMATS[options.format],
|
2021-01-15 01:19:10 -08:00
|
|
|
};
|
|
|
|
|
2021-07-26 20:59:45 -07:00
|
|
|
match_t *m = NULL;
|
|
|
|
while ((m = next_match(defs, f, m, pattern, options.skip, options.ignorecase))) {
|
2021-08-02 12:25:52 -07:00
|
|
|
if (print_errors(f, m) > 0)
|
2021-01-18 10:30:17 -08:00
|
|
|
exit(EXIT_FAILURE);
|
2021-01-15 01:19:10 -08:00
|
|
|
|
2021-08-06 13:15:27 -07:00
|
|
|
if (++matches == 1 && options.print_filenames) {
|
2021-01-15 01:19:10 -08:00
|
|
|
if (printed_filenames++ > 0) printf("\n");
|
2021-08-06 13:15:27 -07:00
|
|
|
fprint_filename(out, f->filename);
|
2021-01-15 01:19:10 -08:00
|
|
|
}
|
2021-08-06 13:15:27 -07:00
|
|
|
print_match(out, &pr, m);
|
2021-01-15 01:19:10 -08:00
|
|
|
}
|
2021-07-26 20:59:45 -07:00
|
|
|
if (m) recycle_if_unused(&m);
|
2021-01-15 01:19:10 -08:00
|
|
|
|
2021-08-23 22:33:58 -07:00
|
|
|
if (matches > 0 || (f->filename[0] == '\0' && options.context_before == ALL_CONTEXT)) {
|
2021-01-15 01:19:10 -08:00
|
|
|
// Print trailing context lines:
|
2021-08-06 13:15:27 -07:00
|
|
|
print_match(out, &pr, NULL);
|
2021-01-15 01:19:10 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return matches;
|
|
|
|
}
|
|
|
|
|
2021-01-12 22:22:38 -08:00
|
|
|
//
|
|
|
|
// For a given filename, open the file and attempt to match the given pattern
|
|
|
|
// against it, printing any results according to the flags.
|
|
|
|
//
|
2021-01-17 21:15:37 -08:00
|
|
|
__attribute__((nonnull(2,3)))
|
2021-01-15 18:38:06 -08:00
|
|
|
static int process_file(def_t *defs, const char *filename, pat_t *pattern)
|
2020-09-13 15:55:09 -07:00
|
|
|
{
|
2021-01-13 18:56:22 -08:00
|
|
|
file_t *f = load_file(NULL, filename);
|
2021-01-17 18:06:00 -08:00
|
|
|
if (f == NULL) {
|
|
|
|
fprintf(stderr, "Could not open file: %s\n%s\n", filename, strerror(errno));
|
|
|
|
return 0;
|
|
|
|
}
|
2021-01-15 01:19:10 -08:00
|
|
|
|
|
|
|
int matches = 0;
|
2021-05-12 20:33:27 -07:00
|
|
|
if (options.mode == MODE_EXPLAIN) {
|
2021-01-15 02:05:17 -08:00
|
|
|
matches += explain_matches(defs, f, pattern);
|
2021-05-12 20:33:27 -07:00
|
|
|
} else if (options.mode == MODE_LISTFILES) {
|
|
|
|
match_t *m = next_match(defs, f, NULL, pattern, options.skip, options.ignorecase);
|
2021-01-15 01:19:10 -08:00
|
|
|
if (m) {
|
|
|
|
recycle_if_unused(&m);
|
|
|
|
printf("%s\n", f->filename);
|
|
|
|
matches += 1;
|
2020-12-12 16:31:53 -08:00
|
|
|
}
|
2021-05-12 20:33:27 -07:00
|
|
|
} else if (options.mode == MODE_JSON) {
|
2021-01-15 02:05:17 -08:00
|
|
|
matches += print_matches_as_json(defs, f, pattern);
|
2021-05-12 20:33:27 -07:00
|
|
|
} else if (options.mode == MODE_INPLACE) {
|
2021-08-06 13:15:27 -07:00
|
|
|
// 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");
|
2021-08-06 17:58:44 -07:00
|
|
|
// 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, defs, f, pattern);
|
|
|
|
}
|
|
|
|
modifying_file = NULL; backup_file = NULL;
|
2021-08-06 13:15:27 -07:00
|
|
|
fclose(out);
|
2021-01-15 01:19:10 -08:00
|
|
|
} else {
|
2021-08-06 13:15:27 -07:00
|
|
|
matches += print_matches(stdout, defs, f, pattern);
|
2020-09-13 15:55:09 -07:00
|
|
|
}
|
2021-07-26 20:59:45 -07:00
|
|
|
fflush(stdout);
|
2021-01-12 18:35:41 -08:00
|
|
|
|
2021-08-01 15:36:53 -07:00
|
|
|
cache_destroy(f);
|
2021-01-26 17:54:23 -08:00
|
|
|
if (recycle_all_matches() != 0)
|
2021-07-26 20:59:45 -07:00
|
|
|
fprintf(stderr, "\033[33;1mMemory leak: there should no longer be any matches in use at this point.\033[0m\n");
|
2021-01-12 18:35:41 -08:00
|
|
|
destroy_file(&f);
|
2021-01-18 09:52:35 -08:00
|
|
|
(void)fflush(stdout);
|
2021-01-15 01:19:10 -08:00
|
|
|
return matches;
|
2020-09-13 15:55:09 -07:00
|
|
|
}
|
|
|
|
|
2021-01-17 17:30:31 -08:00
|
|
|
//
|
|
|
|
// Recursively process all non-dotfile files in the given directory.
|
|
|
|
//
|
2021-01-17 21:15:37 -08:00
|
|
|
__attribute__((nonnull(2,3)))
|
2021-01-17 17:30:31 -08:00
|
|
|
static int process_dir(def_t *defs, const char *dirname, pat_t *pattern)
|
|
|
|
{
|
|
|
|
int matches = 0;
|
|
|
|
glob_t globbuf;
|
2021-01-18 11:28:39 -08:00
|
|
|
char globpath[PATH_MAX+1] = {'\0'};
|
2021-01-26 17:54:23 -08:00
|
|
|
if (snprintf(globpath, PATH_MAX, "%s/*", dirname) > (int)PATH_MAX)
|
|
|
|
errx(EXIT_FAILURE, "Filename is too long: %s/*", dirname);
|
2021-01-18 09:52:35 -08:00
|
|
|
int status = glob(globpath, 0, NULL, &globbuf);
|
2021-01-26 17:54:23 -08:00
|
|
|
if (status == GLOB_ABORTED || status == GLOB_NOSPACE)
|
2021-07-30 15:03:21 -07:00
|
|
|
errx(EXIT_FAILURE, "Failed to get directory contents: %s", dirname);
|
2021-01-18 09:52:35 -08:00
|
|
|
if (status != GLOB_NOMATCH) {
|
|
|
|
struct stat statbuf;
|
|
|
|
for (size_t i = 0; i < globbuf.gl_pathc; i++) {
|
|
|
|
if (lstat(globbuf.gl_pathv[i], &statbuf) != 0) continue;
|
|
|
|
if (S_ISLNK(statbuf.st_mode))
|
|
|
|
continue; // Skip symbolic links
|
|
|
|
else if (S_ISDIR(statbuf.st_mode))
|
|
|
|
matches += process_dir(defs, globbuf.gl_pathv[i], pattern);
|
|
|
|
else if (is_text_file(globbuf.gl_pathv[i]))
|
|
|
|
matches += process_file(defs, globbuf.gl_pathv[i], pattern);
|
|
|
|
}
|
2021-01-17 17:30:31 -08:00
|
|
|
}
|
|
|
|
globfree(&globbuf);
|
|
|
|
return matches;
|
|
|
|
}
|
|
|
|
|
2021-01-26 19:02:56 -08:00
|
|
|
//
|
|
|
|
// Process git files using `git ls-files ...`
|
|
|
|
//
|
|
|
|
__attribute__((nonnull(2)))
|
|
|
|
static int process_git_files(def_t *defs, pat_t *pattern, int argc, char *argv[])
|
|
|
|
{
|
|
|
|
int fds[2];
|
2021-08-06 17:52:20 -07:00
|
|
|
require(pipe(fds), "Failed to create pipe");
|
|
|
|
pid_t child = require(fork(), "Failed to fork");
|
2021-01-26 19:02:56 -08:00
|
|
|
if (child == 0) {
|
2021-08-06 13:26:05 -07:00
|
|
|
const char **git_args = new(char*[3+argc+1]);
|
2021-01-26 19:02:56 -08:00
|
|
|
int g = 0;
|
|
|
|
git_args[g++] = "git";
|
|
|
|
git_args[g++] = "ls-files";
|
2021-08-06 13:26:05 -07:00
|
|
|
git_args[g++] = "-z";
|
2021-01-26 19:02:56 -08:00
|
|
|
while (*argv) git_args[g++] = *(argv++);
|
2021-08-06 17:52:20 -07:00
|
|
|
require(dup2(fds[STDOUT_FILENO], STDOUT_FILENO), "Failed to hook up pipe to stdout");
|
|
|
|
require(close(fds[STDIN_FILENO]), "Failed to close read end of pipe");
|
2021-05-31 13:25:38 -07:00
|
|
|
(void)execvp("git", (char**)git_args);
|
2021-01-26 19:02:56 -08:00
|
|
|
_exit(EXIT_FAILURE);
|
|
|
|
}
|
2021-08-06 17:52:20 -07:00
|
|
|
require(close(fds[STDOUT_FILENO]), "Failed to close write end of pipe");
|
|
|
|
FILE *fp = require(fdopen(fds[STDIN_FILENO], "r"), "Could not open pipe file descriptor");
|
2021-01-26 19:02:56 -08:00
|
|
|
char *path = NULL;
|
2021-08-06 13:26:05 -07:00
|
|
|
size_t path_size = 0;
|
2021-01-26 19:02:56 -08:00
|
|
|
int found = 0;
|
2021-08-06 13:26:05 -07:00
|
|
|
while (getdelim(&path, &path_size, '\0', fp) > 0)
|
2021-01-26 19:02:56 -08:00
|
|
|
found += process_file(defs, path, pattern);
|
2021-07-30 15:06:04 -07:00
|
|
|
if (path) delete(&path);
|
2021-08-06 17:52:20 -07:00
|
|
|
require(fclose(fp), "Failed to close read end of pipe");
|
2021-01-26 19:02:56 -08:00
|
|
|
int status;
|
|
|
|
while (waitpid(child, &status, 0) != child) continue;
|
|
|
|
if (!((WIFEXITED(status) == 1) && (WEXITSTATUS(status) == 0)))
|
2021-08-06 13:26:05 -07:00
|
|
|
errx(EXIT_FAILURE, "`git ls-files -z` failed.");
|
2021-01-26 19:02:56 -08:00
|
|
|
return found;
|
|
|
|
}
|
|
|
|
|
2021-08-02 12:25:52 -07:00
|
|
|
//
|
|
|
|
// Convert a context string to an integer
|
|
|
|
//
|
|
|
|
static int context_from_flag(const char *flag)
|
|
|
|
{
|
|
|
|
if (streq(flag, "all")) return ALL_CONTEXT;
|
|
|
|
if (streq(flag, "none")) return NO_CONTEXT;
|
|
|
|
return (int)strtol(flag, NULL, 10);
|
|
|
|
}
|
|
|
|
|
2021-02-07 11:33:17 -08:00
|
|
|
#define FLAG(f) (flag = get_flag(argv, f, &argv))
|
|
|
|
#define BOOLFLAG(f) get_boolflag(argv, f, &argv)
|
2020-09-09 22:29:09 -07:00
|
|
|
|
|
|
|
int main(int argc, char *argv[])
|
|
|
|
{
|
2020-09-11 02:55:15 -07:00
|
|
|
char *flag = NULL;
|
2020-09-09 22:29:09 -07:00
|
|
|
|
2021-01-10 01:45:40 -08:00
|
|
|
def_t *defs = NULL;
|
2021-01-13 18:56:22 -08:00
|
|
|
file_t *loaded_files = NULL;
|
2021-01-17 13:33:10 -08:00
|
|
|
pat_t *pattern = NULL;
|
2021-01-15 01:19:10 -08:00
|
|
|
|
2020-09-13 23:31:38 -07:00
|
|
|
// Load builtins:
|
2021-07-03 21:43:56 -07:00
|
|
|
file_t *builtins_file = load_filef(&loaded_files, "/etc/"BP_NAME"/builtins.bp");
|
|
|
|
if (builtins_file) defs = load_grammar(defs, builtins_file);
|
2021-01-17 18:06:00 -08:00
|
|
|
file_t *local_file = load_filef(&loaded_files, "%s/.config/"BP_NAME"/builtins.bp", getenv("HOME"));
|
2021-01-15 02:05:17 -08:00
|
|
|
if (local_file) defs = load_grammar(defs, local_file);
|
2020-09-12 15:11:44 -07:00
|
|
|
|
2021-02-07 11:33:17 -08:00
|
|
|
++argv; // skip program name
|
|
|
|
while (argv[0]) {
|
|
|
|
if (streq(argv[0], "--")) {
|
|
|
|
++argv;
|
2020-09-11 02:55:15 -07:00
|
|
|
break;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-h") || BOOLFLAG("--help")) {
|
2021-03-26 00:06:11 -07:00
|
|
|
printf("%s\n\n%s\n", description, usage);
|
2021-01-26 18:50:55 -08:00
|
|
|
exit(EXIT_SUCCESS);
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-v") || BOOLFLAG("--verbose")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.verbose = true;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-e") || BOOLFLAG("--explain")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.mode = MODE_EXPLAIN;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-j") || BOOLFLAG("--json")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.mode = MODE_JSON;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-I") || BOOLFLAG("--inplace")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.mode = MODE_INPLACE;
|
2021-08-06 13:15:27 -07:00
|
|
|
options.print_filenames = false;
|
|
|
|
options.format = FORMAT_PLAIN;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-G") || BOOLFLAG("--git")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.git_mode = true;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-i") || BOOLFLAG("--ignore-case")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.ignorecase = true;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (BOOLFLAG("-l") || BOOLFLAG("--list-files")) {
|
2021-05-12 20:33:27 -07:00
|
|
|
options.mode = MODE_LISTFILES;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (FLAG("-r") || FLAG("--replace")) {
|
2021-01-26 17:54:23 -08:00
|
|
|
if (!pattern)
|
|
|
|
errx(EXIT_FAILURE, "No pattern has been defined for replacement to operate on");
|
2021-01-08 00:57:46 -08:00
|
|
|
// TODO: spoof file as sprintf("pattern => '%s'", flag)
|
|
|
|
// except that would require handling edge cases like quotation marks etc.
|
2021-03-03 17:24:23 -08:00
|
|
|
file_t *replace_file = spoof_file(&loaded_files, "<replace argument>", flag, -1);
|
2021-05-20 18:31:28 -07:00
|
|
|
pattern = bp_replacement(replace_file, pattern, replace_file->start);
|
2021-01-26 17:54:23 -08:00
|
|
|
if (!pattern)
|
|
|
|
errx(EXIT_FAILURE, "Replacement failed to compile: %s", flag);
|
2021-08-06 18:11:29 -07:00
|
|
|
if (options.context_before == USE_DEFAULT_CONTEXT) options.context_before = ALL_CONTEXT;
|
|
|
|
if (options.context_after == USE_DEFAULT_CONTEXT) options.context_after = ALL_CONTEXT;
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (FLAG("-g") || FLAG("--grammar")) {
|
2021-01-17 23:37:35 -08:00
|
|
|
file_t *f = NULL;
|
|
|
|
if (strlen(flag) > 3 && strncmp(&flag[strlen(flag)-3], ".bp", 3) == 0)
|
|
|
|
f = load_file(&loaded_files, flag);
|
2021-01-15 02:05:17 -08:00
|
|
|
if (f == NULL)
|
2021-01-17 18:06:00 -08:00
|
|
|
f = load_filef(&loaded_files, "%s/.config/"BP_NAME"/%s.bp", getenv("HOME"), flag);
|
2021-01-15 02:05:17 -08:00
|
|
|
if (f == NULL)
|
2021-07-03 21:43:56 -07:00
|
|
|
f = load_filef(&loaded_files, "/etc/"BP_NAME"/%s.bp", flag);
|
2021-01-26 17:54:23 -08:00
|
|
|
if (f == NULL)
|
|
|
|
errx(EXIT_FAILURE, "Couldn't find grammar: %s", flag);
|
2021-01-10 01:45:40 -08:00
|
|
|
defs = load_grammar(defs, f); // Keep in memory for debug output
|
2021-01-17 10:27:33 -08:00
|
|
|
} else if (FLAG("-p") || FLAG("--pattern")) {
|
2021-03-03 17:24:23 -08:00
|
|
|
file_t *arg_file = spoof_file(&loaded_files, "<pattern argument>", flag, -1);
|
2021-08-01 12:40:27 -07:00
|
|
|
pat_t *p = bp_pattern(arg_file, arg_file->start);
|
|
|
|
if (!p) file_err(arg_file, arg_file->start, arg_file->end, "Failed to compile this part of the argument");
|
2021-08-06 12:50:57 -07:00
|
|
|
if (after_spaces(p->end, true) < arg_file->end) file_err(arg_file, p->end, arg_file->end, "Failed to compile this part of the argument");
|
2021-08-01 12:40:27 -07:00
|
|
|
pattern = chain_together(arg_file, pattern, p);
|
2021-07-30 20:46:50 -07:00
|
|
|
} else if (FLAG("-w") || FLAG("--word")) {
|
2021-08-06 17:52:20 -07:00
|
|
|
require(asprintf(&flag, "\\|%s\\|", flag), "Could not allocate memory");
|
2021-07-30 20:46:50 -07:00
|
|
|
file_t *arg_file = spoof_file(&loaded_files, "<word pattern>", flag, -1);
|
|
|
|
delete(&flag);
|
|
|
|
pat_t *p = bp_stringpattern(arg_file, arg_file->start);
|
|
|
|
if (!p) errx(EXIT_FAILURE, "Pattern failed to compile: %s", flag);
|
|
|
|
pattern = chain_together(arg_file, pattern, p);
|
2021-01-20 16:12:46 -08:00
|
|
|
} else if (FLAG("-s") || FLAG("--skip")) {
|
2021-03-03 17:24:23 -08:00
|
|
|
file_t *arg_file = spoof_file(&loaded_files, "<skip argument>", flag, -1);
|
2021-05-20 18:31:28 -07:00
|
|
|
pat_t *s = bp_pattern(arg_file, arg_file->start);
|
2021-01-20 16:12:46 -08:00
|
|
|
if (!s) {
|
2021-05-20 18:31:28 -07:00
|
|
|
fprint_line(stdout, arg_file, arg_file->start, arg_file->end,
|
2021-01-20 16:12:46 -08:00
|
|
|
"Failed to compile the skip argument");
|
2021-08-01 13:41:13 -07:00
|
|
|
} else if (after_spaces(s->end, true) < arg_file->end) {
|
2021-01-20 16:12:46 -08:00
|
|
|
fprint_line(stdout, arg_file, s->end, arg_file->end,
|
|
|
|
"Failed to compile part of the skip argument");
|
|
|
|
}
|
2021-05-12 20:33:27 -07:00
|
|
|
options.skip = either_pat(arg_file, options.skip, s);
|
2021-08-02 11:45:01 -07:00
|
|
|
} else if (FLAG("-C") || FLAG("--context")) {
|
2021-08-02 12:25:52 -07:00
|
|
|
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);
|
2021-05-12 20:33:27 -07:00
|
|
|
} else if (FLAG("-f") || FLAG("--format")) {
|
2021-09-02 18:07:18 -07:00
|
|
|
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);
|
2021-02-07 11:33:17 -08:00
|
|
|
} else if (argv[0][0] == '-' && argv[0][1] && argv[0][1] != '-') { // single-char flags
|
|
|
|
errx(EXIT_FAILURE, "Unrecognized flag: -%c\n\n%s", argv[0][1], usage);
|
|
|
|
} else if (argv[0][0] != '-') {
|
2021-01-17 13:33:10 -08:00
|
|
|
if (pattern != NULL) break;
|
2021-03-03 17:24:23 -08:00
|
|
|
file_t *arg_file = spoof_file(&loaded_files, "<pattern argument>", argv[0], -1);
|
2021-05-20 18:31:28 -07:00
|
|
|
pat_t *p = bp_stringpattern(arg_file, arg_file->start);
|
2021-07-30 20:46:50 -07:00
|
|
|
if (!p) errx(EXIT_FAILURE, "Pattern failed to compile: %s", argv[0]);
|
2021-01-17 13:33:10 -08:00
|
|
|
pattern = chain_together(arg_file, pattern, p);
|
2021-02-07 11:33:17 -08:00
|
|
|
++argv;
|
2020-09-12 20:05:55 -07:00
|
|
|
} else {
|
2021-02-07 11:33:17 -08:00
|
|
|
errx(EXIT_FAILURE, "Unrecognized flag: %s\n\n%s", argv[0], usage);
|
2020-09-09 22:29:09 -07:00
|
|
|
}
|
|
|
|
}
|
2020-09-07 23:05:38 -07:00
|
|
|
|
2021-03-26 00:06:11 -07:00
|
|
|
if (pattern == NULL)
|
|
|
|
errx(EXIT_FAILURE, "No pattern provided.\n\n%s", usage);
|
|
|
|
|
2021-02-07 11:33:17 -08:00
|
|
|
for (argc = 0; argv[argc]; ++argc) ; // update argc
|
|
|
|
|
2021-08-02 12:25:52 -07:00
|
|
|
if (options.context_before == USE_DEFAULT_CONTEXT) options.context_before = 0;
|
|
|
|
if (options.context_after == USE_DEFAULT_CONTEXT) options.context_after = 0;
|
2021-01-15 01:19:10 -08:00
|
|
|
|
2021-05-12 20:33:27 -07:00
|
|
|
if (options.format == FORMAT_AUTO)
|
2021-09-04 14:09:20 -07:00
|
|
|
options.format = isatty(STDOUT_FILENO) ? FORMAT_FANCY : FORMAT_BARE;
|
2020-09-09 22:29:09 -07:00
|
|
|
|
2021-01-15 18:23:18 -08:00
|
|
|
// 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++)
|
2021-08-06 17:52:20 -07:00
|
|
|
require(sigaction(signals[i], &sa, NULL), "Failed to set signal handler");
|
2021-01-15 18:23:18 -08:00
|
|
|
|
|
|
|
// Handle exit() calls gracefully:
|
2021-08-06 17:52:20 -07:00
|
|
|
require(atexit(&cleanup), "Failed to set cleanup handler at exit");
|
2021-01-15 18:23:18 -08:00
|
|
|
|
2021-08-01 15:36:53 -07:00
|
|
|
// No need for these caches anymore:
|
|
|
|
for (file_t *f = loaded_files; f; f = f->next)
|
|
|
|
cache_destroy(f);
|
|
|
|
|
2021-01-12 18:35:41 -08:00
|
|
|
int found = 0;
|
2021-05-12 20:33:27 -07:00
|
|
|
if (options.mode == MODE_JSON) printf("[");
|
|
|
|
if (options.git_mode) { // Get the list of files from `git --ls-files ...`
|
2021-02-07 11:33:17 -08:00
|
|
|
found = process_git_files(defs, pattern, argc, argv);
|
|
|
|
} else if (argv[0]) {
|
2020-09-13 15:55:09 -07:00
|
|
|
// Files pass in as command line args:
|
2021-01-17 17:30:31 -08:00
|
|
|
struct stat statbuf;
|
2021-08-25 13:15:05 -07:00
|
|
|
if (!argv[1]) // Don't print filename for single-file matching
|
|
|
|
options.print_filenames = false;
|
2021-02-07 11:33:17 -08:00
|
|
|
for ( ; argv[0]; argv++) {
|
|
|
|
if (stat(argv[0], &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) // Symlinks are okay if manually specified
|
|
|
|
found += process_dir(defs, argv[0], pattern);
|
2021-01-17 17:30:31 -08:00
|
|
|
else
|
2021-02-07 11:33:17 -08:00
|
|
|
found += process_file(defs, argv[0], pattern);
|
2020-09-11 02:55:15 -07:00
|
|
|
}
|
2020-09-13 15:55:09 -07:00
|
|
|
} else if (isatty(STDIN_FILENO)) {
|
2021-01-17 17:30:31 -08:00
|
|
|
// No files, no piped in input, so use files in current dir, recursively
|
|
|
|
found += process_dir(defs, ".", pattern);
|
2020-09-13 15:55:09 -07:00
|
|
|
} else {
|
|
|
|
// Piped in input:
|
2021-08-25 13:15:05 -07:00
|
|
|
options.print_filenames = false; // Don't print filename on stdin
|
2021-01-17 21:15:37 -08:00
|
|
|
found += process_file(defs, "", pattern);
|
2020-09-07 23:05:38 -07:00
|
|
|
}
|
2021-05-12 20:33:27 -07:00
|
|
|
if (options.mode == MODE_JSON) printf("]\n");
|
2020-09-07 23:05:38 -07:00
|
|
|
|
2021-01-14 19:21:31 -08:00
|
|
|
// 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.
|
2021-07-26 20:59:45 -07:00
|
|
|
free_all_matches();
|
|
|
|
defs = free_defs(defs, NULL);
|
2021-01-13 18:56:22 -08:00
|
|
|
while (loaded_files) {
|
|
|
|
file_t *next = loaded_files->next;
|
|
|
|
destroy_file(&loaded_files);
|
|
|
|
loaded_files = next;
|
|
|
|
}
|
2021-01-13 01:48:36 -08:00
|
|
|
|
2021-01-26 18:50:55 -08:00
|
|
|
exit(found > 0 ? EXIT_SUCCESS : EXIT_FAILURE);
|
2020-09-07 23:05:38 -07:00
|
|
|
}
|
2020-09-07 23:34:41 -07:00
|
|
|
|
2021-08-28 16:05:30 -07:00
|
|
|
// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0
|