diff options
Diffstat (limited to 'bp.c')
| -rw-r--r-- | bp.c | 168 |
1 files changed, 148 insertions, 20 deletions
@@ -6,6 +6,7 @@ #include <fcntl.h> #include <glob.h> #include <limits.h> +#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -28,8 +29,9 @@ static const char *usage = ( " -v --verbose print verbose debugging info\n" " -e --explain explain the matches\n" " -j --json print matches as a list of JSON objects\n" - " -I --inplace modify a file in-place\n" " -i --ignore-case preform matching case-insensitively\n" + " -I --inplace modify a file in-place\n" + " -C --confirm ask for confirmation on each replacement\n" " -l --list-files list filenames only\n" " -p --pattern <pat> provide a pattern (equivalent to bp '\\(<pat>)')\n" " -P --pattern-string <pat> provide a string pattern (may be useful if '<pat>' begins with a '-')\n" @@ -45,6 +47,8 @@ static unsigned int print_color = 0; static unsigned int print_line_numbers = 0; static unsigned int ignorecase = 0; static unsigned int verbose = 0; +typedef enum { CONFIRM_ASK, CONFIRM_ALL, CONFIRM_NONE } confirm_t; +static confirm_t confirm = CONFIRM_ALL; static enum { MODE_NORMAL, MODE_LISTFILES, @@ -53,13 +57,27 @@ static enum { MODE_EXPLAIN, } mode = MODE_NORMAL; -__attribute__((nonnull)) -static char *getflag(const char *flag, char *argv[], int *i); +// If a filename is put here, it will be deleted if a signal is received +const char *in_use_tempfile = NULL; + +// Used for user input/output that doesn't interfere with unix pipeline +FILE *tty_out = NULL, *tty_in = NULL; + +// +// Helper function to reduce code duplication +// +static inline void fprint_filename(FILE *out, const char *filename) +{ + if (!filename[0]) return; + if (print_color) fprintf(out, "\033[0;1;4;33m%s\033[0m\n", filename); + else fprintf(out, "%s:\n", filename); +} // // Return a pointer to the value part of a flag, if present, otherwise NULL. // This works for --foo=value or --foo value // +__attribute__((nonnull)) static char *getflag(const char *flag, char *argv[], int *i) { size_t n = strlen(flag); @@ -123,10 +141,7 @@ static int explain_matches(def_t *defs, file_t *f, vm_op_t *pattern) int matches = 0; for (match_t *m = NULL; (m = next_match(defs, f, m, pattern, ignorecase)); ) { if (++matches == 1) { - if (print_color) - printf("\033[0;1;4;33m%s\033[0m\n", f->filename); - else - printf("%s:\n", f->filename); + fprint_filename(stdout, f->filename); } else { printf("\n\n"); } @@ -136,14 +151,85 @@ static int explain_matches(def_t *defs, file_t *f, vm_op_t *pattern) } // +// Cleanup function to ensure no temp files are left around if the program +// exits unexpectedly. +// +static void cleanup(void) +{ + if (in_use_tempfile) { + remove(in_use_tempfile); + in_use_tempfile = NULL; + } +} + +// +// Signal handler to ensure cleanup happens. +// +static void sig_handler(int sig) { (void)sig; cleanup(); } + +// +// Present the user with a prompt to confirm replacements before they happen. +// If the user rejects a replacement, the match object is set to the underlying +// non-replacement value. +// +static void confirm_replacements(file_t *f, match_t *m, confirm_t *confirm) +{ + if (*confirm == CONFIRM_ALL) return; + if (m->op->type == VM_REPLACE) { + if (*confirm == CONFIRM_NONE) { + m->skip_replacement = 1; + goto check_children; + } + + { // Print the original + printer_t pr = {.file = f, .context_lines = 1, + .use_color = 1, .print_line_numbers = 1}; + print_match(tty_out, &pr, m->child); + // Print trailing context lines: + print_match(tty_out, &pr, NULL); + } + { // Print the replacement + printer_t pr = {.file = f, .context_lines = 1, + .use_color = 1, .print_line_numbers = 1}; + print_match(tty_out, &pr, m); + // Print trailing context lines: + print_match(tty_out, &pr, NULL); + } + + retry: + fprintf(tty_out, "\033[1mReplace? (y)es (n)o (r)emaining (d)one\033[0m "); + fflush(tty_out); + + char *answer = NULL; + size_t len = 0; + if (getline(&answer, &len, tty_in) > 0) { + if (strlen(answer) != 2 || answer[1] != '\n') goto retry; + switch (answer[0]) { + case 'y': break; + case 'n': m->skip_replacement = 1; break; + case 'r': *confirm = CONFIRM_ALL; break; + case 'd': m->skip_replacement = 1; *confirm = CONFIRM_NONE; break; + default: goto retry; + } + } + if (answer) xfree(&answer); + fprintf(tty_out, "\n"); + } + + check_children: + if (m->child) + confirm_replacements(f, m->child, confirm); + if (m->nextsibling) + confirm_replacements(f, m->nextsibling, confirm); +} + +// // Replace a file's contents with the text version of a match. // (Useful for replacements) // static int inplace_modify_file(def_t *defs, file_t *f, vm_op_t *pattern) { - // Need to do this before matching: - intern_file(f); - + char tmp_filename[PATH_MAX+1] = {0}; printer_t pr = { .file = f, .context_lines = context_lines, @@ -153,14 +239,27 @@ static int inplace_modify_file(def_t *defs, file_t *f, vm_op_t *pattern) FILE *inplace_file = NULL; // Lazy-open this on the first match int matches = 0; + confirm_t confirm_file = confirm; for (match_t *m = NULL; (m = next_match(defs, f, m, pattern, ignorecase)); ) { ++matches; if (print_errors(&pr, m) > 0) exit(1); // Lazy-open file for writing upon first match: if (inplace_file == NULL) { - inplace_file = fopen(f->filename, "w"); - check(inplace_file, "Could not open file for writing: %s\n", f->filename); + check(snprintf(tmp_filename, PATH_MAX, "%s.tmp.XXXXXX", f->filename) <= PATH_MAX, + "Failed to build temporary file template"); + int out_fd = mkstemp(tmp_filename); + check(out_fd >= 0, "Failed to create temporary inplace file"); + in_use_tempfile = tmp_filename; + inplace_file = fdopen(out_fd, "w"); + if (confirm == CONFIRM_ASK && f->filename) + fprint_filename(tty_out, f->filename); + } + confirm_replacements(f, m, &confirm_file); + if (!in_use_tempfile) { // signal interrupted, so abort + fclose(inplace_file); + inplace_file = NULL; + break; } print_match(inplace_file, &pr, m); } @@ -168,9 +267,18 @@ static int inplace_modify_file(def_t *defs, file_t *f, vm_op_t *pattern) if (inplace_file) { // Print trailing context lines: print_match(inplace_file, &pr, NULL); - printf("%s\n", f->filename); + if (confirm == CONFIRM_ALL) + printf("%s\n", f->filename); fclose(inplace_file); + + // TODO: if I want to implement backup files then add a line like this: + // if (backup) rename(f->filename, f->filename + ".bak"); + check(rename(tmp_filename, f->filename) == 0, + "Failed to write file replacement for %s", f->filename); + + in_use_tempfile = NULL; } + return matches; } @@ -188,25 +296,22 @@ static int print_matches(def_t *defs, file_t *f, vm_op_t *pattern) .print_line_numbers = print_line_numbers, }; + confirm_t confirm_file = confirm; for (match_t *m = NULL; (m = next_match(defs, f, m, pattern, ignorecase)); ) { if (print_errors(&pr, m) > 0) exit(1); if (++matches == 1) { if (printed_filenames++ > 0) printf("\n"); - if (print_color) - printf("\033[0;1;4;33m%s\033[0m\n", f->filename); - else - printf("%s:\n", f->filename); + fprint_filename(stdout, f->filename); } + confirm_replacements(f, m, &confirm_file); print_match(stdout, &pr, m); } if (matches > 0) { // Print trailing context lines: print_match(stdout, &pr, NULL); - // Ensure a trailing newline: - if (pr.pos > f->contents && pr.pos[-1] != '\n') printf("\n"); } return matches; @@ -288,6 +393,8 @@ int main(int argc, char *argv[]) mode = MODE_JSON; } else if (streq(argv[i], "--inplace")) { mode = MODE_INPLACE; + } else if (streq(argv[i], "--confirm")) { + confirm = CONFIRM_ASK; } else if (streq(argv[i], "--ignore-case")) { ignorecase = 1; } else if (streq(argv[i], "--list-files")) { @@ -317,9 +424,9 @@ int main(int argc, char *argv[]) defs = d; str = d->op->end; } else { - check(npatterns == 0, "Cannot define multiple patterns"); vm_op_t *p = bp_pattern(arg_file, str); check(p, "Pattern failed to compile: %s", flag); + check(npatterns == 0, "Cannot define multiple patterns"); defs = with_def(defs, arg_file, strlen("pattern"), "pattern", p); ++npatterns; str = p->end; @@ -347,6 +454,7 @@ int main(int argc, char *argv[]) case 'e': mode = MODE_EXPLAIN; break; // -e case 'j': mode = MODE_JSON; break; // -j case 'I': mode = MODE_INPLACE; break; // -I + case 'C': confirm = CONFIRM_ASK; break; // -C case 'i': ignorecase = 1; break; // -i case 'l': mode = MODE_LISTFILES; break; // -l default: @@ -377,6 +485,23 @@ int main(int argc, char *argv[]) print_line_numbers = 1; } + // 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++) + sigaction(signals[i], &sa, NULL); + + // Handle exit() calls gracefully: + atexit(&cleanup); + + // User input/output is handled through /dev/tty so that normal unix pipes + // can work properly while simultaneously asking for user input. + if (confirm == CONFIRM_ASK) { + tty_in = fopen("/dev/tty", "r"); + tty_out = fopen("/dev/tty", "w"); + } + int found = 0; if (mode == MODE_JSON) printf("["); if (i < argc) { @@ -400,6 +525,9 @@ int main(int argc, char *argv[]) } if (mode == MODE_JSON) printf("]\n"); + if (tty_out) fclose(tty_out); + if (tty_in) fclose(tty_in); + #ifdef DEBUG_HEAP // 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 |
