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

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).
** 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 */
}