Skip to content

Commit

Permalink
Filter command :!
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
martanne committed May 17, 2015
1 parent 55d76f8 commit 1244aea
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 0 deletions.
1 change: 1 addition & 0 deletions config.def.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */ },
};

Expand Down
2 changes: 2 additions & 0 deletions editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define EDITOR_H

#include <curses.h>
#include <signal.h>
#include <stddef.h>
#include <stdbool.h>

Expand Down Expand Up @@ -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*);
Expand Down
216 changes: 216 additions & 0 deletions vis.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <ctype.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit 1244aea

Please sign in to comment.