Implement prefix-based interactive commands via ! (issue #14)

Add !command syntax that runs commands with inherited terminal (no
capture). The lexer treats ! as a statement-starting token, reading
to end-of-line with the same interpolation/escape/pipe support as
backtick commands. The C function sets _ = {code=N, stdout="", stderr=""}.
This commit is contained in:
Cormac Shannon
2026-03-02 22:06:29 +00:00
parent 75098da240
commit db858b3f68
9 changed files with 399 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
# Issue #14 — Prefix-based interactive command execution # Issue #14 — Prefix-based interactive command execution
**Status:** open **Status:** done
**Alternative to:** #10, #12 **Alternative to:** #10, #12
## Motivation ## Motivation

215
lcmd.c
View File

@@ -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). ** are captured. Middle stages' stderr goes to the parent's stderr (inherited).
** Returns 1 (one result table on the Lua stack). ** 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; ParsedArgs *pa = NULL;
int (*inter_pipes)[2] = NULL; /* inter_pipes[i] connects stage i → i+1 */ 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; pid_t *pids = NULL;
int i, last_code = -1; int i, last_code = -1;
DynBuf buf_out, buf_err; 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 */ /* parse all stages */
pa = (ParsedArgs *)calloc((size_t)nstages, sizeof(ParsedArgs)); 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 */ /* capture pipes for last stage (only in non-interactive mode) */
if (pipe(out_pipe) != 0) { if (!interactive) {
if (inter_pipes) { if (pipe(out_pipe) != 0) {
for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } if (inter_pipes) {
free(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]); if (pipe(err_pipe) != 0) {
free(pa); close(out_pipe[0]); close(out_pipe[1]);
return luaL_error(L, "pipe() failed: %s", strerror(errno)); if (inter_pipes) {
} for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); }
if (pipe(err_pipe) != 0) { free(inter_pipes);
close(out_pipe[0]); close(out_pipe[1]); }
if (inter_pipes) { for (i = 0; i < nstages; i++) free_argv(&pa[i]);
for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } free(pa);
free(inter_pipes); 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)); memset(&sa_new, 0, sizeof(sa_new));
sa_new.sa_handler = SIG_IGN; sa_new.sa_handler = SIG_IGN;
sigemptyset(&sa_new.sa_mask); 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 */ /* allocate pid array */
pids = (pid_t *)calloc((size_t)nstages, sizeof(pid_t)); pids = (pid_t *)calloc((size_t)nstages, sizeof(pid_t));
if (pids == NULL) { if (pids == NULL) {
close(out_pipe[0]); close(out_pipe[1]); if (!interactive) {
close(err_pipe[0]); close(err_pipe[1]); close(out_pipe[0]); close(out_pipe[1]);
close(err_pipe[0]); close(err_pipe[1]);
}
if (inter_pipes) { if (inter_pipes) {
for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); } for (i = 0; i < nstages - 1; i++) { close(inter_pipes[i][0]); close(inter_pipes[i][1]); }
free(inter_pipes); free(inter_pipes);
} }
for (i = 0; i < nstages; i++) free_argv(&pa[i]); for (i = 0; i < nstages; i++) free_argv(&pa[i]);
free(pa); 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"); 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); kill(pids[j], SIGTERM);
waitpid(pids[j], NULL, 0); waitpid(pids[j], NULL, 0);
} }
close(out_pipe[0]); close(out_pipe[1]); if (!interactive) {
close(err_pipe[0]); close(err_pipe[1]); close(out_pipe[0]); close(out_pipe[1]);
close(err_pipe[0]); close(err_pipe[1]);
}
if (inter_pipes) { if (inter_pipes) {
for (int j = 0; j < nstages - 1; j++) { close(inter_pipes[j][0]); close(inter_pipes[j][1]); } for (int j = 0; j < nstages - 1; j++) { close(inter_pipes[j][0]); close(inter_pipes[j][1]); }
free(inter_pipes); free(inter_pipes);
} }
for (int j = 0; j < nstages; j++) free_argv(&pa[j]); for (int j = 0; j < nstages; j++) free_argv(&pa[j]);
free(pa); free(pids); 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)); 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 === */ /* === child process for stage i === */
int j; int j;
/* restore default SIGPIPE */ /* restore default signals */
sa_new.sa_handler = SIG_DFL; sa_new.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &sa_new, NULL); sigaction(SIGPIPE, &sa_new, NULL);
if (interactive) {
sigaction(SIGINT, &sa_new, NULL);
sigaction(SIGQUIT, &sa_new, NULL);
}
/* set up stdin */ /* set up stdin */
if (i > 0) { if (i > 0) {
@@ -443,11 +466,12 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) {
/* set up stdout */ /* set up stdout */
if (i < nstages - 1) { if (i < nstages - 1) {
dup2(inter_pipes[i][1], STDOUT_FILENO); dup2(inter_pipes[i][1], STDOUT_FILENO);
} else { } else if (!interactive) {
/* last stage: capture stdout and stderr */ /* last stage: capture stdout and stderr */
dup2(out_pipe[1], STDOUT_FILENO); dup2(out_pipe[1], STDOUT_FILENO);
dup2(err_pipe[1], STDERR_FILENO); dup2(err_pipe[1], STDERR_FILENO);
} }
/* else: interactive last stage inherits terminal */
/* close all pipe fds in child */ /* close all pipe fds in child */
if (inter_pipes) { if (inter_pipes) {
@@ -456,8 +480,10 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) {
close(inter_pipes[j][1]); close(inter_pipes[j][1]);
} }
} }
close(out_pipe[0]); close(out_pipe[1]); if (!interactive) {
close(err_pipe[0]); close(err_pipe[1]); close(out_pipe[0]); close(out_pipe[1]);
close(err_pipe[0]); close(err_pipe[1]);
}
execvp(pa[i].argv[0], pa[i].argv); 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); free(inter_pipes);
} }
close(out_pipe[1]);
close(err_pipe[1]);
/* free parsed args (children have already exec'd) */ /* free parsed args (children have already exec'd) */
for (i = 0; i < nstages; i++) free_argv(&pa[i]); for (i = 0; i < nstages; i++) free_argv(&pa[i]);
free(pa); free(pa);
/* read captured output from last stage */
dynbuf_init(&buf_out); dynbuf_init(&buf_out);
dynbuf_init(&buf_err); 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 */ /* wait for all children, keep last stage's exit code */
for (i = 0; i < nstages; i++) { for (i = 0; i < nstages; i++) {
@@ -510,8 +539,12 @@ static int exec_pipeline (lua_State *L, char **stages, int nstages) {
} }
free(pids); free(pids);
/* restore SIGPIPE */ /* restore signals */
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);
}
/* push result table */ /* push result table */
push_result_table(L, last_code, &buf_out, &buf_err); 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 */ /* multi-stage pipeline: delegate to exec_pipeline */
if (nstages > 1) { if (nstages > 1) {
int result = exec_pipeline(L, stages, nstages); int result = exec_pipeline(L, stages, nstages, 0);
free(stages[0]); /* free backing buffer */ free(stages[0]); /* free backing buffer */
return result; return result;
} }
@@ -657,3 +690,107 @@ int luaB_command (lua_State *L) {
return 1; 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 */
}

1
lcmd.h
View File

@@ -10,5 +10,6 @@
#include "lua.h" #include "lua.h"
int luaB_command (lua_State *L); int luaB_command (lua_State *L);
int luaB_interactive (lua_State *L);
#endif #endif

View File

@@ -71,6 +71,8 @@ static int luaB_setenv (lua_State *L) {
static void opencommand (lua_State *L) { static void opencommand (lua_State *L) {
lua_pushcfunction(L, luaB_command); lua_pushcfunction(L, luaB_command);
lua_setglobal(L, "__command"); lua_setglobal(L, "__command");
lua_pushcfunction(L, luaB_interactive);
lua_setglobal(L, "__interactive");
lua_pushcfunction(L, luaB_getenv); lua_pushcfunction(L, luaB_getenv);
lua_setglobal(L, "__getenv"); lua_setglobal(L, "__getenv");
lua_pushcfunction(L, luaB_setenv); lua_pushcfunction(L, luaB_setenv);

36
llex.c
View File

@@ -50,7 +50,8 @@ static const char *const luaX_tokens [] = {
"//", "..", "...", "==", ">=", "<=", "~=", "//", "..", "...", "==", ">=", "<=", "~=",
"<<", ">>", "::", "<eof>", "<<", ">>", "::", "<eof>",
"<number>", "<integer>", "<name>", "<string>", "<number>", "<integer>", "<name>", "<string>",
"<command>", "<command_interp>", "<envvar>" "<command>", "<command_interp>", "<envvar>",
"<interactive>", "<interactive_interp>"
}; };
@@ -190,7 +191,7 @@ void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
ls->source = source; ls->source = source;
/* all three strings here ("_ENV", "break", "global") were fixed, /* all three strings here ("_ENV", "break", "global") were fixed,
so they cannot be collected */ so they cannot be collected */
ls->in_command = 0; ls->cmd_mode = 0;
ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */ ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */
ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */ ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */
#if defined(LUA_COMPAT_GLOBAL) #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. ** or TK_COMMAND_INTERP if an interpolation ${...} was found.
*/ */
static int read_command_body (LexState *ls, SemInfo *seminfo) { static int read_command_body (LexState *ls, SemInfo *seminfo) {
int interactive = (ls->cmd_mode == 2);
luaZ_resetbuffer(ls->buff); luaZ_resetbuffer(ls->buff);
for (;;) { for (;;) {
switch (ls->current) { switch (ls->current) {
case EOZ: 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); lexerror(ls, "unfinished command", TK_EOS);
break; /* to avoid warnings */ break; /* to avoid warnings */
case '\n': case '\n':
case '\r': 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'); save(ls, '\n');
inclinenumber(ls); inclinenumber(ls);
break; break;
@@ -496,8 +512,7 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
/* store fragment so far */ /* store fragment so far */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff)); luaZ_bufflen(ls->buff));
ls->in_command = 1; return interactive ? TK_INTERACTIVE_INTERP : TK_COMMAND_INTERP;
return TK_COMMAND_INTERP;
} }
else { else {
save(ls, '$'); /* not an interpolation, keep literal '$' */ save(ls, '$'); /* not an interpolation, keep literal '$' */
@@ -505,10 +520,15 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
break; break;
} }
case '`': { case '`': {
if (interactive) {
/* backtick is literal in interactive mode */
save_and_next(ls);
break;
}
next(ls); /* skip closing '`' */ next(ls); /* skip closing '`' */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff)); luaZ_bufflen(ls->buff));
ls->in_command = 0; ls->cmd_mode = 0;
return TK_COMMAND; return TK_COMMAND;
} }
case '\\': { /* escape sequences */ case '\\': { /* escape sequences */
@@ -541,6 +561,7 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
static int read_command (LexState *ls, SemInfo *seminfo) { static int read_command (LexState *ls, SemInfo *seminfo) {
next(ls); /* skip opening '`' */ next(ls); /* skip opening '`' */
ls->cmd_mode = 1;
return read_command_body(ls, seminfo); return read_command_body(ls, seminfo);
} }
@@ -636,6 +657,11 @@ static int llex (LexState *ls, SemInfo *seminfo) {
case '`': { /* explicit command `ls -l` with ${} interpolation */ case '`': { /* explicit command `ls -l` with ${} interpolation */
return read_command(ls, seminfo); 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 */ case '$': { /* environment variable $NAME */
next(ls); next(ls);
if (lislalpha(ls->current)) { /* $NAME */ if (lislalpha(ls->current)) { /* $NAME */

6
llex.h
View File

@@ -42,7 +42,9 @@ enum RESERVED {
TK_FLT, TK_INT, TK_NAME, TK_STRING, TK_FLT, TK_INT, TK_NAME, TK_STRING,
TK_COMMAND, TK_COMMAND,
TK_COMMAND_INTERP, TK_COMMAND_INTERP,
TK_ENVVAR TK_ENVVAR,
TK_INTERACTIVE,
TK_INTERACTIVE_INTERP
}; };
/* number of reserved words */ /* number of reserved words */
@@ -80,7 +82,7 @@ typedef struct LexState {
TString *envn; /* environment variable name */ TString *envn; /* environment variable name */
TString *brkn; /* "break" name (used as a label) */ TString *brkn; /* "break" name (used as a label) */
TString *glbn; /* "global" name (when not a reserved word) */ 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; } LexState;

View File

@@ -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) { static void statement (LexState *ls) {
int line = ls->linenumber; /* may be needed for error messages */ int line = ls->linenumber; /* may be needed for error messages */
enterlevel(ls); enterlevel(ls);
@@ -2275,6 +2354,11 @@ static void statement (LexState *ls) {
envstat(ls); envstat(ls);
break; break;
} }
case TK_INTERACTIVE:
case TK_INTERACTIVE_INTERP: { /* stat -> !command */
interactivestat(ls);
break;
}
#if defined(LUA_COMPAT_GLOBAL) #if defined(LUA_COMPAT_GLOBAL)
case TK_NAME: { case TK_NAME: {
/* compatibility code to parse global keyword when "global" /* compatibility code to parse global keyword when "global"

View File

@@ -124,9 +124,12 @@ lexerror([['alo \98]], "<eof>")
-- valid characters in variable names -- valid characters in variable names
for i = 0, 255 do for i = 0, 255 do
local s = string.char(i) 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_]") == not load(s .. "=1", ""))
assert(not string.find(s, "[a-zA-Z_0-9]") == assert(not string.find(s, "[a-zA-Z_0-9]") ==
not load("a" .. s .. "1 = 1", "")) not load("a" .. s .. "1 = 1", ""))
::continue::
end end

View File

@@ -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"