aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2021-01-15 18:23:18 -0800
committerBruce Hill <bruce@bruce-hill.com>2021-01-15 18:23:18 -0800
commitd31d2e89850d3132850e0f2e1ce6d973bd482073 (patch)
tree654739b075372bbe55b4082b2c77d7e70813e7fb
parent87ad1efc24521c0a17c1d8d627518f1eb9f4fa4f (diff)
Added interactive confirmation mode for replacing text
-rw-r--r--README.md1
-rw-r--r--bp.14
-rw-r--r--bp.c168
-rw-r--r--printing.c14
-rw-r--r--types.h5
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<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.
diff --git a/bp.c b/bp.c
index ce67ce2..77589ca 100644
--- a/bp.c
+++ b/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");
}
@@ -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
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;
//