From c56b57fc58c73e96def91f4904d5b6e7c50e3de9 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Wed, 22 May 2024 11:37:50 +0200 Subject: [PATCH 1/4] support piping a buffer to an external process Currently only Text objects can be piped to external commands. This is tedious if data not available in any file should be passed to an external process (e.g. building options and passing them to vis-menu). This adds the option to pass a buffer to _vis_pipe and provides wrapper functions for the original behavior and the new one. --- text-io.c | 2 +- text.h | 6 +++++ vis-lua.c | 30 +++++++++++++++++++--- vis.c | 74 +++++++++++++++++++++++++++++++++++++++++-------------- vis.h | 14 +++++++++++ 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/text-io.c b/text-io.c index 509df3549..97fc138a9 100644 --- a/text-io.c +++ b/text-io.c @@ -182,7 +182,7 @@ Text *text_load_method(const char *filename, enum TextLoadMethod method) { return text_loadat_method(AT_FDCWD, filename, method); } -static ssize_t write_all(int fd, const char *buf, size_t count) { +ssize_t write_all(int fd, const char *buf, size_t count) { size_t rem = count; while (rem > 0) { ssize_t written = write(fd, buf, rem > INT_MAX ? INT_MAX : rem); diff --git a/text.h b/text.h index 048aef763..3249e21cf 100644 --- a/text.h +++ b/text.h @@ -413,6 +413,12 @@ ssize_t text_write_range(const Text*, const Filerange*, int fd); * this text instance. */ bool text_mmaped(const Text*, const char *ptr); + +/** + * Write complete buffer to file descriptor. + * @return The number of bytes written or ``-1`` in case of an error. + */ +ssize_t write_all(int fd, const char *buf, size_t count); /** @} */ #endif diff --git a/vis-lua.c b/vis-lua.c index e3e85c345..0a435b460 100644 --- a/vis-lua.c +++ b/vis-lua.c @@ -1234,14 +1234,34 @@ static int exit_func(lua_State *L) { * @treturn string stdout the data written to stdout * @treturn string stderr the data written to stderr */ +/*** + * Pipe a string to external process and collect output. + * + * The editor core will be blocked while the external process is running. + * + * @function pipe + * @tparam string text the text written to the external command + * @tparam string command the command to execute + * @tparam[opt] bool fullscreen whether command is a fullscreen program (e.g. curses based) + * @treturn int code the exit status of the executed command + * @treturn string stdout the data written to stdout + * @treturn string stderr the data written to stderr + */ static int pipe_func(lua_State *L) { Vis *vis = obj_ref_check(L, 1, "vis"); int cmd_idx = 4; char *out = NULL, *err = NULL; + const char *text = NULL; File *file = vis->win ? vis->win->file : NULL; Filerange range = text_range_new(0, 0); - if (lua_gettop(L) <= 3) { + if (lua_gettop(L) == 2) { cmd_idx = 2; + } else if (lua_gettop(L) == 3) { + text = luaL_checkstring(L, 2); + if (text != NULL) + cmd_idx = 3; + else + cmd_idx = 2; } else if (!(lua_isnil(L, 2) && lua_isnil(L, 3))) { file = obj_ref_check(L, 2, VIS_LUA_TYPE_FILE); range = getrange(L, 3); @@ -1249,10 +1269,14 @@ static int pipe_func(lua_State *L) { const char *cmd = luaL_checkstring(L, cmd_idx); bool fullscreen = lua_isboolean(L, cmd_idx + 1) && lua_toboolean(L, cmd_idx + 1); - if (!file) + if (!text && !file) return luaL_error(L, "vis:pipe(cmd = '%s'): win not open, file can't be nil", cmd); - int status = vis_pipe_collect(vis, file, &range, (const char*[]){ cmd, NULL }, &out, &err, fullscreen); + int status; + if (text) + status = vis_pipe_buf_collect(vis, text, (const char*[]){ cmd, NULL }, &out, &err, fullscreen); + else + status = vis_pipe_collect(vis, file, &range, (const char*[]){ cmd, NULL }, &out, &err, fullscreen); lua_pushinteger(L, status); if (out) lua_pushstring(L, out); diff --git a/vis.c b/vis.c index 7d3dd6a96..10c3df575 100644 --- a/vis.c +++ b/vis.c @@ -1593,17 +1593,17 @@ Regex *vis_regex(Vis *vis, const char *pattern) { return regex; } -int vis_pipe(Vis *vis, File *file, Filerange *range, const char *argv[], +static int _vis_pipe(Vis *vis, File *file, Filerange *range, const char* buf, const char *argv[], void *stdout_context, ssize_t (*read_stdout)(void *stdout_context, char *data, size_t len), void *stderr_context, ssize_t (*read_stderr)(void *stderr_context, char *data, size_t len), bool fullscreen) { /* if an invalid range was given, stdin (i.e. key board input) is passed * through the external command. */ - Text *text = file->text; + Text *text = file != NULL ? file->text : NULL; int pin[2], pout[2], perr[2], status = -1; - bool interactive = !text_range_valid(range); - Filerange rout = interactive ? text_range_new(0, 0) : *range; + bool interactive = buf == NULL && (range == NULL || !text_range_valid(range)); + Filerange rout = (interactive || buf != NULL) ? text_range_new(0, 0) : *range; if (pipe(pin) == -1) return -1; @@ -1654,7 +1654,7 @@ int vis_pipe(Vis *vis, File *file, Filerange *range, const char *argv[], * closed. Some programs behave differently when used * in a pipeline. */ - if (text_range_size(range) == 0) + if (range && text_range_size(range) == 0) dup2(null, STDIN_FILENO); else dup2(pin[0], STDIN_FILENO); @@ -1688,7 +1688,7 @@ int vis_pipe(Vis *vis, File *file, Filerange *range, const char *argv[], close(perr[1]); close(null); - if (file->name) { + if (file != NULL && file->name) { char *name = strrchr(file->name, '/'); setenv("vis_filepath", file->name, 1); setenv("vis_filename", name ? name+1 : file->name, 1); @@ -1737,20 +1737,36 @@ int vis_pipe(Vis *vis, File *file, Filerange *range, const char *argv[], } if (pin[1] != -1 && FD_ISSET(pin[1], &wfds)) { + ssize_t written = 0; Filerange junk = rout; - if (junk.end > junk.start + PIPE_BUF) - junk.end = junk.start + PIPE_BUF; - ssize_t len = text_write_range(text, &junk, pin[1]); - if (len > 0) { - rout.start += len; - if (text_range_size(&rout) == 0) { - close(pin[1]); - pin[1] = -1; + if (text_range_size(&rout)) { + if (junk.end > junk.start + PIPE_BUF) + junk.end = junk.start + PIPE_BUF; + written = text_write_range(text, &junk, pin[1]); + if (written > 0) { + rout.start += written; + if (text_range_size(&rout) == 0) { + close(pin[1]); + pin[1] = -1; + } } - } else { + } else if (buf != NULL) { + size_t len = strlen(buf); + if (len > 0) { + if (len > PIPE_BUF) + len = PIPE_BUF; + + written = write_all(pin[1], buf, len); + if (written > 0) { + buf += written; + } + } + } + + if (written <= 0) { close(pin[1]); pin[1] = -1; - if (len == -1) + if (written == -1) vis_info_show(vis, "Error writing to external command"); } } @@ -1823,16 +1839,30 @@ int vis_pipe(Vis *vis, File *file, Filerange *range, const char *argv[], return -1; } +int vis_pipe(Vis *vis, File *file, Filerange *range, const char *argv[], + void *stdout_context, ssize_t (*read_stdout)(void *stdout_context, char *data, size_t len), + void *stderr_context, ssize_t (*read_stderr)(void *stderr_context, char *data, size_t len), + bool fullscreen) { + return _vis_pipe(vis, file, range, NULL, argv, stdout_context, read_stdout, stderr_context, read_stderr, fullscreen); +} + +int vis_pipe_buf(Vis *vis, const char* buf, const char *argv[], + void *stdout_context, ssize_t (*read_stdout)(void *stdout_context, char *data, size_t len), + void *stderr_context, ssize_t (*read_stderr)(void *stderr_context, char *data, size_t len), + bool fullscreen) { + return _vis_pipe(vis, NULL, NULL, buf, argv, stdout_context, read_stdout, stderr_context, read_stderr, fullscreen); +} + static ssize_t read_buffer(void *context, char *data, size_t len) { buffer_append(context, data, len); return len; } -int vis_pipe_collect(Vis *vis, File *file, Filerange *range, const char *argv[], char **out, char **err, bool fullscreen) { +static int _vis_pipe_collect(Vis *vis, File *file, Filerange *range, const char* buf, const char *argv[], char **out, char **err, bool fullscreen) { Buffer bufout, buferr; buffer_init(&bufout); buffer_init(&buferr); - int status = vis_pipe(vis, file, range, argv, + int status = _vis_pipe(vis, file, range, buf, argv, &bufout, out ? read_buffer : NULL, &buferr, err ? read_buffer : NULL, fullscreen); @@ -1847,6 +1877,14 @@ int vis_pipe_collect(Vis *vis, File *file, Filerange *range, const char *argv[], return status; } +int vis_pipe_collect(Vis *vis, File *file, Filerange *range, const char *argv[], char **out, char **err, bool fullscreen) { + return _vis_pipe_collect(vis, file, range, NULL, argv, out, err, fullscreen); +} + +int vis_pipe_buf_collect(Vis *vis, const char* buf, const char *argv[], char **out, char **err, bool fullscreen) { + return _vis_pipe_collect(vis, NULL, NULL, buf, argv, out, err, fullscreen); +} + bool vis_cmd(Vis *vis, const char *cmdline) { if (!cmdline) return true; diff --git a/vis.h b/vis.h index eb8ec92d1..1a249ee64 100644 --- a/vis.h +++ b/vis.h @@ -882,6 +882,20 @@ int vis_pipe(Vis*, File*, Filerange*, const char *argv[], */ int vis_pipe_collect(Vis*, File*, Filerange*, const char *argv[], char **out, char **err, bool fullscreen); +/** + * Pipe a buffer to an external process, return its exit status and capture + * everything that is written to stdout/stderr. + * @param argv Argument list, must be ``NULL`` terminated. + * @param out Data written to ``stdout``, will be ``NUL`` terminated. + * @param err Data written to ``stderr``, will be ``NUL`` terminated. + * @param fullscreen Whether the external process is a fullscreen program (e.g. curses based) + * @rst + * .. warning:: The pointers stored in ``out`` and ``err`` need to be `free(3)`-ed + * by the caller. + * @endrst + */ +int vis_pipe_buf_collect(Vis*, const char*, const char *argv[], char **out, char **err, bool fullscreen); + /** * @} * @defgroup vis_keys From c569e85f6a1521b30437335b247732f6af635a61 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Sun, 8 Sep 2024 18:45:30 +0200 Subject: [PATCH 2/4] complete-word: pipe the candidates directly to the command --- lua/plugins/complete-word.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/plugins/complete-word.lua b/lua/plugins/complete-word.lua index 5e355e87e..511b47e4c 100644 --- a/lua/plugins/complete-word.lua +++ b/lua/plugins/complete-word.lua @@ -24,8 +24,7 @@ vis:map(vis.modes.INSERT, "", function() if #candidates == 1 and candidates[1] == "\n" then return end candidates = table.concat(candidates, "\n") - local cmd = "printf '" .. candidates .. "' | sort -u | vis-menu" - local status, out, err = vis:pipe(cmd) + local status, out, err = vis:pipe(candidates, "sort -u | vis-menu") if status ~= 0 or not out then if err then vis:info(err) end return From 06453ed994acbaa48a590fa71d4dfb18e1ff3a64 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Tue, 10 Sep 2024 09:52:49 +0200 Subject: [PATCH 3/4] lua: improve argument parsing in vis.pipe Support the old behavior of using vis:pipe(cmd, fullscreen) without input. Properly distinguish between vis:pipe(text, cmd, fullscreen) and vis:pipe(file, range, cmd). --- vis-lua.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/vis-lua.c b/vis-lua.c index 0a435b460..f3478863a 100644 --- a/vis-lua.c +++ b/vis-lua.c @@ -1254,15 +1254,19 @@ static int pipe_func(lua_State *L) { const char *text = NULL; File *file = vis->win ? vis->win->file : NULL; Filerange range = text_range_new(0, 0); - if (lua_gettop(L) == 2) { + if (lua_gettop(L) == 2) { // vis:pipe(cmd) cmd_idx = 2; } else if (lua_gettop(L) == 3) { - text = luaL_checkstring(L, 2); - if (text != NULL) - cmd_idx = 3; - else + if (lua_isboolean(L, 3)) { // vis:pipe(cmd, fullscreen) cmd_idx = 2; - } else if (!(lua_isnil(L, 2) && lua_isnil(L, 3))) { + } else { // vis:pipe(text, cmd) + text = luaL_checkstring(L, 2); + cmd_idx = 3; + } + } else if (lua_isboolean(L, 4)) { // vis:pipe(text, cmd, fullscreen) + text = luaL_checkstring(L, 2); + cmd_idx = 3; + } else if (!(lua_isnil(L, 2) && lua_isnil(L, 3))) { // vis:pipe(file, range, cmd, [fullscreen]) file = obj_ref_check(L, 2, VIS_LUA_TYPE_FILE); range = getrange(L, 3); } From c8694ee0f0fb0540bf0d4e7a25f65924eda02caf Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Sun, 8 Sep 2024 19:03:54 +0200 Subject: [PATCH 4/4] lua: add tests for the different vis.pipe argument variants --- test/lua/pipe.in | 1 + test/lua/pipe.lua | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 test/lua/pipe.in create mode 100644 test/lua/pipe.lua diff --git a/test/lua/pipe.in b/test/lua/pipe.in new file mode 100644 index 000000000..191028156 --- /dev/null +++ b/test/lua/pipe.in @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/test/lua/pipe.lua b/test/lua/pipe.lua new file mode 100644 index 000000000..9dfb670bc --- /dev/null +++ b/test/lua/pipe.lua @@ -0,0 +1,86 @@ +require 'busted.runner'() + +local file = vis.win.file + +describe("vis.pipe", function() + + local FULLSCREEN = true + + it("vis.pipe no input", function() + vis:pipe("cat > f") + local f = io.open("f", "r") + assert.truthy(f) + assert.are.equal("", f:read("*a")) + f:close() + os.remove("f") + end) + + it("vis.pipe no input fullscreen", function() + vis:pipe("cat > f", FULLSCREEN) + local f = io.open("f", "r") + assert.truthy(f) + assert.are.equal("", f:read("*a")) + f:close() + os.remove("f") + end) + + it("vis.pipe buffer", function() + vis:pipe("foo", "cat > f") + local f = io.open("f", "r") + assert.truthy(f) + assert.are.equal(f:read("*a"), "foo") + f:close() + os.remove("f") + end) + + it("vis.pipe buffer fullscreen", function() + vis:pipe("foo", "cat > f", FULLSCREEN) + local f = io.open("f", "r") + assert.truthy(f) + assert.are.equal(f:read("*a"), "foo") + f:close() + os.remove("f") + end) + + it("vis.pipe range", function() + vis:pipe(file, {start=0, finish=3}, "cat > f") + local f = io.open("f", "r") + assert.truthy(f) + assert.are.equal(f:read("*a"), "foo") + f:close() + os.remove("f") + end) + + it("vis.pipe range fullscreen", function() + vis:pipe(file, {start=0, finish=3}, "cat > f", FULLSCREEN) + local f = io.open("f", "r") + assert.truthy(f) + assert.are.equal(f:read("*a"), "foo") + f:close() + os.remove("f") + end) + + it("vis.pipe explicit nil text", function() + assert.has_error(function() vis:pipe(nil, "true") end) + end) + + it("vis.pipe explicit nil text fullscreen", function() + assert.has_error(function() vis:pipe(nil, "true", FULLSCREEN) end) + end) + + it("vis.pipe explicit nil file", function() + assert.has_error(function() vis:pipe(nil, {start=0, finish=0}, "true") end) + end) + + it("vis.pipe explicit nil file fullscreen", function() + assert.has_error(function() vis:pipe(nil, {start=0, finish=0}, "true", FULLSCREEN) end) + end) + + it("vis.pipe wrong argument order file, range, cmd", function() + assert.has_error(function() vis:pipe({start=0, finish=0}, vis.win.file, "true") end) + end) + + it("vis.pipe wrong argument order fullscreen, cmd", function() + assert.has_error(function() vis:pipe(FULLSCREEN, "true") end) + end) +end)