From d31d2e89850d3132850e0f2e1ce6d973bd482073 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Fri, 15 Jan 2021 18:23:18 -0800 Subject: Added interactive confirmation mode for replacing text --- README.md | 1 + bp.1 | 4 ++ bp.c | 168 +++++++++++++++++++++++++++++++++++++++++++++++++++++-------- printing.c | 14 ++++-- types.h | 5 ++ 5 files changed, 168 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 650fb53..3317209 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ It's written in pure C with no dependencies. * `-v` `--verbose` print verbose debugging info * `-i` `--ignore-case` perform a case-insensitive match * `-I` `--inplace` perform replacements or filtering in-place on files +* `-C` `--confirm` during replacement, confirm before each replacement * `-e` `--explain` print an explanation of the matches * `-j` `--json` print matches as JSON objects * `-l` `--list-files` print only filenames containing matches diff --git a/bp.1 b/bp.1 index 5b220ad..68f452e 100644 --- a/bp.1 +++ b/bp.1 @@ -12,6 +12,7 @@ bp \- Bruce's Parsing Expression Grammar tool [\fI-l\fR|\fI--list-files\fR] [\fI-i\fR|\fI--ignore-case\fR \fI\fR] [\fI-I\fR|\fI--inplace\fR] +[\fI-C\fR|\fI--confirm\fR] [\fI-p\fR|\fI--pattern\fR \fI\fR] [\fI-P\fR|\fI--pattern-string\fR \fI\fR] [\fI-r\fR|\fI--replace\fR \fI\fR] @@ -40,6 +41,9 @@ Perform pattern matching case-insensitively. .B \-I\fR, \fB--inplace Perform filtering or replacement in-place (i.e. overwrite files with new content). +.B \-C\fR, \fB--confirm +During in-place modification of a file, confirm before each modification. + .B \-r\fR, \fB--replace \fI\fR Replace all occurrences of the main pattern with the given string. diff --git a/bp.c b/bp.c index ce67ce2..77589ca 100644 --- a/bp.c +++ b/bp.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -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 provide a pattern (equivalent to bp '\\()')\n" " -P --pattern-string provide a string pattern (may be useful if '' 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"); } @@ -135,15 +150,86 @@ static int explain_matches(def_t *defs, file_t *f, vm_op_t *pattern) return matches; } +// +// 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 diff --git a/printing.c b/printing.c index ad1598c..2cfa93a 100644 --- a/printing.c +++ b/printing.c @@ -261,11 +261,15 @@ void _print_match(FILE *out, printer_t *pr, match_t *m) { pr->pos = m->start; if (m->op->type == VM_REPLACE) { + if (m->skip_replacement) { + _print_match(out, pr, m->child); + return; + } size_t line_start = get_line_number(pr->file, m->start); size_t line_end = get_line_number(pr->file, m->end); size_t line = line_start; - if (pr->use_color) printf("%s", color_replace); + if (pr->use_color) fprintf(out, "%s", color_replace); const char *text = m->op->args.replace.text; const char *end = &text[m->op->args.replace.len]; @@ -279,7 +283,7 @@ void _print_match(FILE *out, printer_t *pr, match_t *m) match_t *cap = get_capture(m, &r); if (cap != NULL) { _print_match(out, pr, cap); - if (pr->use_color) printf("%s", color_replace); + if (pr->use_color) fprintf(out, "%s", color_replace); continue; } else { --r; @@ -347,17 +351,19 @@ void print_match(FILE *out, printer_t *pr, match_t *m) // Non-overlapping ranges: print_between(out, pr, pr->pos, after_last, pr->use_color ? color_normal : NULL); if (pr->context_lines > 1) - printf("\n"); // Gap between chunks + fprintf(out, "\n"); // Gap between chunks } } print_between(out, pr, before_m, m->start, pr->use_color ? color_normal : NULL); _print_match(out, pr, m); - if (pr->use_color) printf("%s", color_normal); } else { // After the last match is printed, print the trailing context: const char *after_last = context_after(pr, pr->pos); print_between(out, pr, pr->pos, after_last, pr->use_color ? color_normal : NULL); + // Guarantee trailing newline + if (pr->pos > pr->file->contents && pr->pos[-1] != '\n') fprintf(out, "\n"); } + if (pr->use_color) fprintf(out, "%s", color_normal); } // diff --git a/types.h b/types.h index 27b3d8e..d9c528b 100644 --- a/types.h +++ b/types.h @@ -89,6 +89,11 @@ typedef struct match_s { struct match_s **atme; #endif int refcount; + // If skip_replacement is set to 1, that means the user wants to not print + // the replaced text when printing this match: + // TODO: this is a bit hacky, there is probably a better way to go about + // this but it's less hacky that mutating the match objects more drastically + unsigned int skip_replacement:1; } match_t; // -- cgit v1.2.3