Added interactive confirmation mode for replacing text
This commit is contained in:
parent
87ad1efc24
commit
d31d2e8985
@ -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
4
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<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
168
bp.c
@ -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
|
||||
|
14
printing.c
14
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);
|
||||
}
|
||||
|
||||
//
|
||||
|
5
types.h
5
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;
|
||||
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user