From 1244aeae5c603593aa71df4b67974c11ea1576cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Andr=C3=A9=20Tanner?= Date: Sat, 16 May 2015 18:01:13 +0200 Subject: [PATCH] Filter command :! If no range is given then stdin is passed through which allows interactive usage as in :!ls -1 *.c | slmenu For this to work the command needs to use stderr for its user interface and write any data for vis to stdout. --- config.def.h | 1 + editor.h | 2 + vis.c | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) diff --git a/config.def.h b/config.def.h index 88c032cd3..d2fab76aa 100644 --- a/config.def.h +++ b/config.def.h @@ -67,6 +67,7 @@ static Command cmds[] = { { { "wq", }, cmd_wq, CMD_OPT_FORCE }, { { "write", "w" }, cmd_write, CMD_OPT_FORCE }, { { "xit", }, cmd_xit, CMD_OPT_FORCE }, + { { "!", }, cmd_filter, CMD_OPT_NONE }, { /* array terminator */ }, }; diff --git a/editor.h b/editor.h index 7104d0192..d9857553e 100644 --- a/editor.h +++ b/editor.h @@ -2,6 +2,7 @@ #define EDITOR_H #include +#include #include #include @@ -244,6 +245,7 @@ struct Editor { Mode *mode_prev; /* previsouly active user mode */ Mode *mode_before_prompt; /* user mode which was active before entering prompt */ volatile bool running; /* exit main loop once this becomes false */ + volatile sig_atomic_t cancel_filter; /* abort external command */ }; Editor *editor_new(Ui*); diff --git a/vis.c b/vis.c index ee30c2078..d78ed6b4c 100644 --- a/vis.c +++ b/vis.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -413,6 +414,8 @@ static bool cmd_write(Filerange*, enum CmdOpt, const char *argv[]); * associate the new name with the buffer. further :w commands * without arguments will write to the new filename */ static bool cmd_saveas(Filerange*, enum CmdOpt, const char *argv[]); +/* filter range through external program argv[1] */ +static bool cmd_filter(Filerange*, enum CmdOpt, const char *argv[]); static void action_reset(Action *a); static void switchmode_to(Mode *new_mode); @@ -1641,6 +1644,218 @@ static bool cmd_saveas(Filerange *range, enum CmdOpt opt, const char *argv[]) { return false; } +static void cancel_filter(int sig) { + vis->cancel_filter = true; +} + +static bool cmd_filter(Filerange *range, enum CmdOpt opt, const char *argv[]) { + /* if an invalid range was given, stdin (i.e. key board input) is passed + * through the external command. */ + Text *text = vis->win->file->text; + View *view = vis->win->view; + int pin[2], pout[2], perr[2], status = -1; + bool interactive = !text_range_valid(range); + size_t pos = view_cursor_get(view); + + if (pipe(pin) == -1) + return false; + if (pipe(pout) == -1) { + close(pin[0]); + close(pin[1]); + return false; + } + + if (pipe(perr) == -1) { + close(pin[0]); + close(pin[1]); + close(pout[0]); + close(pout[1]); + return false; + } + + reset_shell_mode(); + pid_t pid = fork(); + + if (pid == -1) { + close(pin[0]); + close(pin[1]); + close(pout[0]); + close(pout[1]); + close(perr[0]); + close(perr[1]); + editor_info_show(vis, "fork failure: %s", strerror(errno)); + return false; + } else if (pid == 0) { /* child i.e filter */ + if (!interactive) + dup2(pin[0], STDIN_FILENO); + close(pin[0]); + close(pin[1]); + dup2(pout[1], STDOUT_FILENO); + close(pout[1]); + close(pout[0]); + if (!interactive) + dup2(perr[1], STDERR_FILENO); + close(perr[0]); + close(perr[1]); + if (!argv[2]) + execl("/bin/sh", "sh", "-c", argv[1], NULL); + else + execvp(argv[1], (char**)argv+1); + editor_info_show(vis, "exec failure: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + + /* set up a signal handler to cancel the filter via CTRL-C */ + struct sigaction sa, oldsa; + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sa.sa_handler = cancel_filter; + + bool restore_signals = sigaction(SIGINT, &sa, &oldsa) == 0; + vis->cancel_filter = false; + + close(pin[0]); + close(pout[1]); + close(perr[1]); + + fcntl(pout[0], F_SETFL, O_NONBLOCK); + fcntl(perr[0], F_SETFL, O_NONBLOCK); + + if (interactive) + range = &(Filerange){ .start = pos, .end = pos }; + + /* ranges which are written to the filter and read back in */ + Filerange rout = *range; + Filerange rin = (Filerange){ .start = range->end, .end = range->end }; + + /* The general idea is the following: + * + * 1) take a snapshot + * 2) write [range.start, range.end] to exteneral command + * 3) read the output of the external command and insert it after the range + * 4) depending on the exit status of the external command + * - on success: delete original range + * - on failure: revert to previous snapshot + * + * 2) and 3) happend in small junks + */ + + text_snapshot(text); + + fd_set rfds, wfds; + Buffer errmsg; + buffer_init(&errmsg); + + do { + if (vis->cancel_filter) { + kill(-pid, SIGTERM); + editor_info_show(vis, "Command cancelled"); + break; + } + + FD_ZERO(&rfds); + FD_ZERO(&wfds); + if (pin[1] != -1) + FD_SET(pin[1], &wfds); + if (pout[0] != -1) + FD_SET(pout[0], &rfds); + if (perr[0] != -1) + FD_SET(perr[0], &rfds); + + if (select(FD_SETSIZE, &rfds, &wfds, NULL, NULL) == -1) { + if (errno == EINTR) + continue; + editor_info_show(vis, "Select failure"); + break; + } + + if (FD_ISSET(pin[1], &wfds)) { + Filerange junk = *range; + if (junk.end > junk.start + PIPE_BUF) + junk.end = junk.start + PIPE_BUF; + ssize_t len = text_range_write(text, &junk, pin[1]); + if (len > 0) { + range->start += len; + if (text_range_size(range) == 0) { + close(pout[1]); + pout[1] = -1; + } + } else { + close(pin[1]); + pin[1] = -1; + if (len == -1) + editor_info_show(vis, "Error writing to external command"); + } + } + + if (FD_ISSET(pout[0], &rfds)) { + char buf[BUFSIZ]; + ssize_t len = read(pout[0], buf, sizeof buf); + if (len > 0) { + text_insert(text, rin.end, buf, len); + rin.end += len; + } else if (len == 0) { + close(pout[0]); + pout[0] = -1; + } else if (errno != EINTR && errno != EWOULDBLOCK) { + editor_info_show(vis, "Error reading from filter stdout"); + close(pout[0]); + pout[0] = -1; + } + } + + if (FD_ISSET(perr[0], &rfds)) { + char buf[BUFSIZ]; + ssize_t len = read(perr[0], buf, sizeof buf); + if (len > 0) { + buffer_append(&errmsg, buf, len); + } else if (len == 0) { + close(perr[0]); + perr[0] = -1; + } else if (errno != EINTR && errno != EWOULDBLOCK) { + editor_info_show(vis, "Error reading from filter stderr"); + close(pout[0]); + pout[0] = -1; + } + } + + } while (pin[1] != -1 || pout[0] != -1 || perr[0] != -1); + + if (pin[1] != -1) + close(pin[1]); + if (pout[0] != -1) + close(pout[0]); + if (perr[0] != -1) + close(perr[0]); + + if (waitpid(pid, &status, 0) == pid && status == 0) { + text_delete(text, rout.start, rout.end - rout.start); + text_snapshot(text); + } else { + /* make sure we have somehting to undo */ + text_insert(text, pos, " ", 1); + text_undo(text); + } + + view_cursor_to(view, rout.start); + + if (!vis->cancel_filter) { + if (status == 0) + editor_info_show(vis, "Command succeded"); + else if (errmsg.len > 0) + editor_info_show(vis, "Command failed: %s", errmsg.data); + else + editor_info_show(vis, "Command failed"); + } + + if (restore_signals) + sigaction(SIGTERM, &oldsa, NULL); + + reset_prog_mode(); + wclear(stdscr); + return status == 0; +} + static Filepos parse_pos(char **cmd) { size_t pos = EPOS; View *view = vis->win->view; @@ -1983,6 +2198,7 @@ static void mainloop() { sigemptyset(&blockset); sigaddset(&blockset, SIGWINCH); sigprocmask(SIG_BLOCK, &blockset, NULL); + signal(SIGPIPE, SIG_IGN); editor_draw(vis); vis->running = true;