Added interactive confirmation mode for replacing text

This commit is contained in:
Bruce Hill 2021-01-15 18:23:18 -08:00
parent 87ad1efc24
commit d31d2e8985
5 changed files with 168 additions and 24 deletions

View File

@ -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

4
bp.1
View File

@ -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<pattern>\fR]
[\fI-I\fR|\fI--inplace\fR]
[\fI-C\fR|\fI--confirm\fR]
[\fI-p\fR|\fI--pattern\fR \fI<pattern>\fR]
[\fI-P\fR|\fI--pattern-string\fR \fI<string-pattern>\fR]
[\fI-r\fR|\fI--replace\fR \fI<replacement>\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<replacement>\fR
Replace all occurrences of the main pattern with the given string.

168
bp.c
View File

@ -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");
}
@ -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

View File

@ -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);
}
//

View File

@ -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;
//