diff --git a/issues/14-prefix-interactive-commands.md b/issues/14-prefix-interactive-commands.md index 7c79f51e..231b67ce 100644 --- a/issues/14-prefix-interactive-commands.md +++ b/issues/14-prefix-interactive-commands.md @@ -1,6 +1,6 @@ # Issue #14 — Prefix-based interactive command execution -**Status:** open +**Status:** done **Alternative to:** #10, #12 ## Motivation diff --git a/lcmd.c b/lcmd.c index ab49dc3b..925ec2d2 100644 --- a/lcmd.c +++ b/lcmd.c @@ -319,14 +319,15 @@ static void push_result_table (lua_State *L, int code, ** are captured. Middle stages' stderr goes to the parent's stderr (inherited). ** Returns 1 (one result table on the Lua stack). */ -static int exec_pipeline (lua_State *L, char **stages, int nstages) { +static int exec_pipeline (lua_State *L, char **stages, int nstages, + int interactive) { ParsedArgs *pa = NULL; int (*inter_pipes)[2] = NULL; /* inter_pipes[i] connects stage i → i+1 */ - int out_pipe[2], err_pipe[2]; /* capture pipes for last stage */ + int out_pipe[2] = {-1, -1}, err_pipe[2] = {-1, -1}; pid_t *pids = NULL; int i, last_code = -1; DynBuf buf_out, buf_err; - struct sigaction sa_old, sa_new; + struct sigaction sa_old_pipe, sa_old_int, sa_old_quit, sa_new; /* parse all stages */ pa = (ParsedArgs *)calloc((size_t)nstages, sizeof(ParsedArgs)); @@ -364,45 +365,57 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { } } - /* capture pipes for last stage */ - if (pipe(out_pipe) != 0) { - if (inter_pipes) { - for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } - free(inter_pipes); + /* capture pipes for last stage (only in non-interactive mode) */ + if (!interactive) { + if (pipe(out_pipe) != 0) { + if (inter_pipes) { + for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } + free(inter_pipes); + } + for (i = 0; i < nstages; i++) free_argv(&pa[i]); + free(pa); + return luaL_error(L, "pipe() failed: %s", strerror(errno)); } - for (i = 0; i < nstages; i++) free_argv(&pa[i]); - free(pa); - return luaL_error(L, "pipe() failed: %s", strerror(errno)); - } - if (pipe(err_pipe) != 0) { - close(out_pipe[0]); close(out_pipe[1]); - if (inter_pipes) { - for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } - free(inter_pipes); + if (pipe(err_pipe) != 0) { + close(out_pipe[0]); close(out_pipe[1]); + if (inter_pipes) { + for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } + free(inter_pipes); + } + for (i = 0; i < nstages; i++) free_argv(&pa[i]); + free(pa); + return luaL_error(L, "pipe() failed: %s", strerror(errno)); } - for (i = 0; i < nstages; i++) free_argv(&pa[i]); - free(pa); - return luaL_error(L, "pipe() failed: %s", strerror(errno)); } - /* ignore SIGPIPE in parent */ + /* ignore signals in parent */ memset(&sa_new, 0, sizeof(sa_new)); sa_new.sa_handler = SIG_IGN; sigemptyset(&sa_new.sa_mask); - sigaction(SIGPIPE, &sa_new, &sa_old); + sigaction(SIGPIPE, &sa_new, &sa_old_pipe); + if (interactive) { + sigaction(SIGINT, &sa_new, &sa_old_int); + sigaction(SIGQUIT, &sa_new, &sa_old_quit); + } /* allocate pid array */ pids = (pid_t *)calloc((size_t)nstages, sizeof(pid_t)); if (pids == NULL) { - close(out_pipe[0]); close(out_pipe[1]); - close(err_pipe[0]); close(err_pipe[1]); + if (!interactive) { + close(out_pipe[0]); close(out_pipe[1]); + close(err_pipe[0]); close(err_pipe[1]); + } if (inter_pipes) { for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } free(inter_pipes); } for (i = 0; i < nstages; i++) free_argv(&pa[i]); free(pa); - sigaction(SIGPIPE, &sa_old, NULL); + sigaction(SIGPIPE, &sa_old_pipe, NULL); + if (interactive) { + sigaction(SIGINT, &sa_old_int, NULL); + sigaction(SIGQUIT, &sa_old_quit, NULL); + } return luaL_error(L, "out of memory"); } @@ -415,15 +428,21 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { kill(pids[j], SIGTERM); waitpid(pids[j], NULL, 0); } - close(out_pipe[0]); close(out_pipe[1]); - close(err_pipe[0]); close(err_pipe[1]); + if (!interactive) { + close(out_pipe[0]); close(out_pipe[1]); + close(err_pipe[0]); close(err_pipe[1]); + } if (inter_pipes) { for (int j = 0; j < nstages - 1; j++) { close(inter_pipes[j][0]); close(inter_pipes[j][1]); } free(inter_pipes); } for (int j = 0; j < nstages; j++) free_argv(&pa[j]); free(pa); free(pids); - sigaction(SIGPIPE, &sa_old, NULL); + sigaction(SIGPIPE, &sa_old_pipe, NULL); + if (interactive) { + sigaction(SIGINT, &sa_old_int, NULL); + sigaction(SIGQUIT, &sa_old_quit, NULL); + } return luaL_error(L, "fork() failed: %s", strerror(errno)); } @@ -431,9 +450,13 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { /* === child process for stage i === */ int j; - /* restore default SIGPIPE */ + /* restore default signals */ sa_new.sa_handler = SIG_DFL; sigaction(SIGPIPE, &sa_new, NULL); + if (interactive) { + sigaction(SIGINT, &sa_new, NULL); + sigaction(SIGQUIT, &sa_new, NULL); + } /* set up stdin */ if (i > 0) { @@ -443,11 +466,12 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { /* set up stdout */ if (i < nstages - 1) { dup2(inter_pipes[i][1], STDOUT_FILENO); - } else { + } else if (!interactive) { /* last stage: capture stdout and stderr */ dup2(out_pipe[1], STDOUT_FILENO); dup2(err_pipe[1], STDERR_FILENO); } + /* else: interactive last stage inherits terminal */ /* close all pipe fds in child */ if (inter_pipes) { @@ -456,8 +480,10 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { close(inter_pipes[j][1]); } } - close(out_pipe[0]); close(out_pipe[1]); - close(err_pipe[0]); close(err_pipe[1]); + if (!interactive) { + close(out_pipe[0]); close(out_pipe[1]); + close(err_pipe[0]); close(err_pipe[1]); + } execvp(pa[i].argv[0], pa[i].argv); @@ -483,17 +509,20 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { } free(inter_pipes); } - close(out_pipe[1]); - close(err_pipe[1]); /* free parsed args (children have already exec'd) */ for (i = 0; i < nstages; i++) free_argv(&pa[i]); free(pa); - /* read captured output from last stage */ dynbuf_init(&buf_out); dynbuf_init(&buf_err); - read_pipes(out_pipe[0], err_pipe[0], &buf_out, &buf_err); + + if (!interactive) { + close(out_pipe[1]); + close(err_pipe[1]); + /* read captured output from last stage */ + read_pipes(out_pipe[0], err_pipe[0], &buf_out, &buf_err); + } /* wait for all children, keep last stage's exit code */ for (i = 0; i < nstages; i++) { @@ -510,8 +539,12 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) { } free(pids); - /* restore SIGPIPE */ - sigaction(SIGPIPE, &sa_old, NULL); + /* restore signals */ + sigaction(SIGPIPE, &sa_old_pipe, NULL); + if (interactive) { + sigaction(SIGINT, &sa_old_int, NULL); + sigaction(SIGQUIT, &sa_old_quit, NULL); + } /* push result table */ push_result_table(L, last_code, &buf_out, &buf_err); @@ -541,7 +574,7 @@ int luaB_command (lua_State *L) { /* multi-stage pipeline: delegate to exec_pipeline */ if (nstages > 1) { - int result = exec_pipeline(L, stages, nstages); + int result = exec_pipeline(L, stages, nstages, 0); free(stages[0]); /* free backing buffer */ return result; } @@ -657,3 +690,107 @@ int luaB_command (lua_State *L) { return 1; } + + +/* ===== luaB_interactive ===== */ + +int luaB_interactive (lua_State *L) { + const char *cmd = luaL_checkstring(L, 1); + char *stages[MAX_PIPELINE_STAGES]; + int nstages; + ParsedArgs pa; + pid_t pid; + int status, code; + struct sigaction sa_old_pipe, sa_old_int, sa_old_quit, sa_new; + DynBuf buf_out, buf_err; + + /* try to split into pipeline stages */ + if (split_pipeline(cmd, stages, &nstages) != 0) + return luaL_error(L, "invalid pipeline syntax in command"); + + /* multi-stage pipeline: delegate to exec_pipeline with interactive=1 */ + if (nstages > 1) { + exec_pipeline(L, stages, nstages, 1); + free(stages[0]); + lua_setglobal(L, "_"); + return 0; + } + + /* single stage */ + free(stages[0]); + + if (parse_argv(cmd, &pa) != 0) + return luaL_error(L, "unterminated quote in command"); + + if (pa.argc == 0) { + free_argv(&pa); + dynbuf_init(&buf_out); + dynbuf_init(&buf_err); + push_result_table(L, 0, &buf_out, &buf_err); + lua_setglobal(L, "_"); + return 0; + } + + /* ignore SIGINT, SIGQUIT, SIGPIPE in parent */ + memset(&sa_new, 0, sizeof(sa_new)); + sa_new.sa_handler = SIG_IGN; + sigemptyset(&sa_new.sa_mask); + sigaction(SIGPIPE, &sa_new, &sa_old_pipe); + sigaction(SIGINT, &sa_new, &sa_old_int); + sigaction(SIGQUIT, &sa_new, &sa_old_quit); + + pid = fork(); + if (pid < 0) { + free_argv(&pa); + sigaction(SIGPIPE, &sa_old_pipe, NULL); + sigaction(SIGINT, &sa_old_int, NULL); + sigaction(SIGQUIT, &sa_old_quit, NULL); + return luaL_error(L, "fork() failed: %s", strerror(errno)); + } + + if (pid == 0) { + /* child: restore signal defaults, inherit terminal */ + sa_new.sa_handler = SIG_DFL; + sigaction(SIGPIPE, &sa_new, NULL); + sigaction(SIGINT, &sa_new, NULL); + sigaction(SIGQUIT, &sa_new, NULL); + + execvp(pa.argv[0], pa.argv); + + /* exec failed */ + { + const char *err = strerror(errno); + size_t namelen = strlen(pa.argv[0]); + size_t errlen = strlen(err); + (void)write(STDERR_FILENO, pa.argv[0], namelen); + (void)write(STDERR_FILENO, ": ", 2); + (void)write(STDERR_FILENO, err, errlen); + (void)write(STDERR_FILENO, "\n", 1); + } + _exit(127); + } + + /* parent */ + free_argv(&pa); + waitpid(pid, &status, 0); + + /* restore signals */ + sigaction(SIGPIPE, &sa_old_pipe, NULL); + sigaction(SIGINT, &sa_old_int, NULL); + sigaction(SIGQUIT, &sa_old_quit, NULL); + + if (WIFEXITED(status)) + code = WEXITSTATUS(status); + else if (WIFSIGNALED(status)) + code = 128 + WTERMSIG(status); + else + code = -1; + + /* build {code=N, stdout="", stderr=""} and set as global _ */ + dynbuf_init(&buf_out); + dynbuf_init(&buf_err); + push_result_table(L, code, &buf_out, &buf_err); + lua_setglobal(L, "_"); + + return 0; /* void — no Lua return values */ +} diff --git a/lcmd.h b/lcmd.h index aa7e5b3a..565dd417 100644 --- a/lcmd.h +++ b/lcmd.h @@ -10,5 +10,6 @@ #include "lua.h" int luaB_command (lua_State *L); +int luaB_interactive (lua_State *L); #endif diff --git a/linit.c b/linit.c index 725deb35..42972d6e 100644 --- a/linit.c +++ b/linit.c @@ -71,6 +71,8 @@ static int luaB_setenv (lua_State *L) { static void opencommand (lua_State *L) { lua_pushcfunction(L, luaB_command); lua_setglobal(L, "__command"); + lua_pushcfunction(L, luaB_interactive); + lua_setglobal(L, "__interactive"); lua_pushcfunction(L, luaB_getenv); lua_setglobal(L, "__getenv"); lua_pushcfunction(L, luaB_setenv); diff --git a/llex.c b/llex.c index fe1924de..77e70f1e 100644 --- a/llex.c +++ b/llex.c @@ -50,7 +50,8 @@ static const char *const luaX_tokens [] = { "//", "..", "...", "==", ">=", "<=", "~=", "<<", ">>", "::", "", "", "", "", "", - "", "", "" + "", "", "", + "", "" }; @@ -190,7 +191,7 @@ void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source, ls->source = source; /* all three strings here ("_ENV", "break", "global") were fixed, so they cannot be collected */ - ls->in_command = 0; + ls->cmd_mode = 0; ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */ ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */ #if defined(LUA_COMPAT_GLOBAL) @@ -478,14 +479,29 @@ static void read_string (LexState *ls, int del, SemInfo *seminfo) { ** or TK_COMMAND_INTERP if an interpolation ${...} was found. */ static int read_command_body (LexState *ls, SemInfo *seminfo) { + int interactive = (ls->cmd_mode == 2); luaZ_resetbuffer(ls->buff); for (;;) { switch (ls->current) { case EOZ: + if (interactive) { + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), + luaZ_bufflen(ls->buff)); + ls->cmd_mode = 0; + return TK_INTERACTIVE; + } lexerror(ls, "unfinished command", TK_EOS); break; /* to avoid warnings */ case '\n': case '\r': + if (interactive) { + /* newline terminates interactive command (don't consume it — + leave for inclinenumber on next llex() call) */ + seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), + luaZ_bufflen(ls->buff)); + ls->cmd_mode = 0; + return TK_INTERACTIVE; + } save(ls, '\n'); inclinenumber(ls); break; @@ -496,8 +512,7 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) { /* store fragment so far */ seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), luaZ_bufflen(ls->buff)); - ls->in_command = 1; - return TK_COMMAND_INTERP; + return interactive ? TK_INTERACTIVE_INTERP : TK_COMMAND_INTERP; } else { save(ls, '$'); /* not an interpolation, keep literal '$' */ @@ -505,10 +520,15 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) { break; } case '`': { + if (interactive) { + /* backtick is literal in interactive mode */ + save_and_next(ls); + break; + } next(ls); /* skip closing '`' */ seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), luaZ_bufflen(ls->buff)); - ls->in_command = 0; + ls->cmd_mode = 0; return TK_COMMAND; } case '\\': { /* escape sequences */ @@ -541,6 +561,7 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) { static int read_command (LexState *ls, SemInfo *seminfo) { next(ls); /* skip opening '`' */ + ls->cmd_mode = 1; return read_command_body(ls, seminfo); } @@ -636,6 +657,11 @@ static int llex (LexState *ls, SemInfo *seminfo) { case '`': { /* explicit command `ls -l` with ${} interpolation */ return read_command(ls, seminfo); } + case '!': { /* interactive command !ls -l */ + next(ls); /* skip '!' */ + ls->cmd_mode = 2; + return read_command_body(ls, seminfo); + } case '$': { /* environment variable $NAME */ next(ls); if (lislalpha(ls->current)) { /* $NAME */ diff --git a/llex.h b/llex.h index c8a74ebb..33fb09d0 100644 --- a/llex.h +++ b/llex.h @@ -42,7 +42,9 @@ enum RESERVED { TK_FLT, TK_INT, TK_NAME, TK_STRING, TK_COMMAND, TK_COMMAND_INTERP, - TK_ENVVAR + TK_ENVVAR, + TK_INTERACTIVE, + TK_INTERACTIVE_INTERP }; /* number of reserved words */ @@ -80,7 +82,7 @@ typedef struct LexState { TString *envn; /* environment variable name */ TString *brkn; /* "break" name (used as a label) */ TString *glbn; /* "global" name (when not a reserved word) */ - lu_byte in_command; /* nonzero when continuing a backtick string after ${} */ + lu_byte cmd_mode; /* 0=normal, 1=backtick command, 2=interactive command */ } LexState; diff --git a/lparser.c b/lparser.c index ae88577a..ade7a7e9 100644 --- a/lparser.c +++ b/lparser.c @@ -2206,6 +2206,85 @@ static void envstat (LexState *ls) { } +/* +** Parse an interactive command statement (!cmd). +** Compiles !cmd as __interactive("cmd") with result discarded. +** The C function sets global _ = {code=N, stdout="", stderr=""}. +*/ +static void interactivestat (LexState *ls) { + FuncState *fs = ls->fs; + if (ls->t.token == TK_INTERACTIVE) { + /* simple interactive command, no interpolation */ + int base, line; + expdesc func, cmdstr, v; + TString *fname = luaX_newstring(ls, "__interactive", 13); + line = ls->linenumber; + buildglobal(ls, fname, &func); + luaK_exp2nextreg(fs, &func); + base = func.u.info; + codestring(&cmdstr, ls->t.seminfo.ts); + luaK_exp2nextreg(fs, &cmdstr); + luaX_next(ls); /* consume TK_INTERACTIVE */ + init_exp(&v, VCALL, luaK_codeABC(fs, OP_CALL, base, 2, 1)); + luaK_fixline(fs, line); + fs->freereg = cast_byte(base); + } + else { + /* interactive command with ${expr} interpolation */ + expdesc func, cmdstr, v; + int base, nconcat = 0; + int line = ls->linenumber; + TString *fname = luaX_newstring(ls, "__interactive", 13); + TString *tsname = luaX_newstring(ls, "tostring", 8); + buildglobal(ls, fname, &func); + luaK_exp2nextreg(fs, &func); + base = func.u.info; + /* load the first fragment */ + codestring(&cmdstr, ls->t.seminfo.ts); + luaK_exp2nextreg(fs, &cmdstr); + nconcat++; + for (;;) { + expdesc interp, tostrfn; + int tostr_base; + luaX_next(ls); /* advance past TK_INTERACTIVE_INTERP */ + buildglobal(ls, tsname, &tostrfn); + luaK_exp2nextreg(fs, &tostrfn); + tostr_base = tostrfn.u.info; + expr(ls, &interp); + check(ls, '}'); + luaK_exp2nextreg(fs, &interp); + luaK_codeABC(fs, OP_CALL, tostr_base, 2, 2); + fs->freereg = cast_byte(tostr_base + 1); + nconcat++; + if (luaX_readcommandcont(ls) == TK_INTERACTIVE_INTERP) { + expdesc frag; + codestring(&frag, ls->t.seminfo.ts); + luaK_exp2nextreg(fs, &frag); + nconcat++; + } + else { + /* TK_INTERACTIVE: final fragment */ + expdesc frag; + codestring(&frag, ls->t.seminfo.ts); + luaK_exp2nextreg(fs, &frag); + nconcat++; + break; + } + } + luaX_next(ls); /* advance past final TK_INTERACTIVE */ + if (nconcat > 1) { + int first = base + 1; + luaK_codeABC(fs, OP_CONCAT, first, nconcat, 0); + fs->freereg = cast_byte(first + 1); + } + /* emit void call (C=1 means 0 return values) */ + init_exp(&v, VCALL, luaK_codeABC(fs, OP_CALL, base, 2, 1)); + luaK_fixline(fs, line); + fs->freereg = cast_byte(base); + } +} + + static void statement (LexState *ls) { int line = ls->linenumber; /* may be needed for error messages */ enterlevel(ls); @@ -2275,6 +2354,11 @@ static void statement (LexState *ls) { envstat(ls); break; } + case TK_INTERACTIVE: + case TK_INTERACTIVE_INTERP: { /* stat -> !command */ + interactivestat(ls); + break; + } #if defined(LUA_COMPAT_GLOBAL) case TK_NAME: { /* compatibility code to parse global keyword when "global" diff --git a/testes/literals.lua b/testes/literals.lua index 336ef585..63743c2c 100644 --- a/testes/literals.lua +++ b/testes/literals.lua @@ -124,9 +124,12 @@ lexerror([['alo \98]], "") -- valid characters in variable names for i = 0, 255 do local s = string.char(i) + -- skip '!' which is now a valid statement starter (interactive commands) + if s == '!' then goto continue end assert(not string.find(s, "[a-zA-Z_]") == not load(s .. "=1", "")) assert(not string.find(s, "[a-zA-Z_0-9]") == not load("a" .. s .. "1 = 1", "")) + ::continue:: end diff --git a/testes/lush/interactive.lua b/testes/lush/interactive.lua new file mode 100644 index 00000000..04f680ee --- /dev/null +++ b/testes/lush/interactive.lua @@ -0,0 +1,97 @@ +-- testes/lush/interactive.lua +-- Tests for prefix-based interactive command execution (issue #14). + +print "testing interactive commands" + +-- ===== BASIC: _ IS SET CORRECTLY ===== + +-- basic interactive command sets _ +!true +assert(type(_) == "table") +assert(_.code == 0) +assert(_.stdout == "") +assert(_.stderr == "") + +-- exit code preserved +!false +assert(_.code == 1) + +-- specific exit code +!sh -c "exit 42" +assert(_.code == 42) + +-- command not found +!nonexistent_command_xyz_999 +assert(_.code == 127) + +-- ===== INTERPOLATION ===== + +do + local name = "hello" +!echo ${name} + assert(_.code == 0) + assert(_.stdout == "") +end + +-- interpolation with expression +do + local x = 21 +!sh -c "exit ${x * 2}" + assert(_.code == 42) +end + +-- ===== PIPING ===== + +-- exit code from last stage +!echo hello | sh -c "exit 7" +assert(_.code == 7) + +-- piping: first stage output goes to second +!echo hello | cat +assert(_.code == 0) + +-- ===== _ IS OVERWRITTEN ===== + +!true +assert(_.code == 0) +!false +assert(_.code == 1) + +-- ===== INSIDE LUA BLOCKS ===== + +do +!true + assert(_.code == 0) +end + +do + if true then +!true + assert(_.code == 0) + end +end + +do + for i = 1, 3 do +!true + assert(_.code == 0) + end +end + +do + local function run_cmd() +!true + return _.code + end + assert(run_cmd() == 0) +end + +-- ===== EMPTY COMMAND (just whitespace after !) ===== + +do + -- set _ to something first, then run empty interactive + !true + assert(_.code == 0) +end + +print "OK"