Files
lush/lcmd.c
Cormac Shannon db858b3f68 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=""}.
2026-03-02 22:06:29 +00:00

797 lines
21 KiB
C

/*
** $Id: lcmd.c $
** Command execution for backtick syntax
** See Copyright Notice in lua.h
*/
#define lcmd_c
#define LUA_LIB
#include "lprefix.h"
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/wait.h>
#include <unistd.h>
#include "lua.h"
#include "lauxlib.h"
#include "lcmd.h"
/* ===== argv parser ===== */
typedef struct {
char **argv;
char *buf;
int argc;
} ParsedArgs;
static void free_argv (ParsedArgs *pa) {
free(pa->buf);
free(pa->argv);
}
/*
** Tokenize command string into argv array.
** Handles: whitespace splitting, single quotes (literal),
** double quotes (with \", \\, \n, \t, \$ escapes), backslash escaping.
** Returns 0 on success, -1 on error (unterminated quote).
*/
static int parse_argv (const char *cmd, ParsedArgs *result) {
size_t len = strlen(cmd);
char *buf = (char *)malloc(len + 1);
int capacity = 8;
char **argv = (char **)malloc((size_t)capacity * sizeof(char *));
int argc = 0;
size_t bp = 0; /* position in buf */
const char *p = cmd;
if (buf == NULL || argv == NULL) {
free(buf);
free(argv);
return -1;
}
while (*p != '\0') {
/* skip whitespace */
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p == '\0')
break;
/* start of a token */
if (argc >= capacity - 1) {
capacity *= 2;
argv = (char **)realloc(argv, (size_t)capacity * sizeof(char *));
if (argv == NULL) { free(buf); return -1; }
}
argv[argc++] = buf + bp;
while (*p != '\0') {
if (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
break;
if (*p == '\'') {
/* single quote: literal until closing quote */
p++;
while (*p != '\0' && *p != '\'')
buf[bp++] = *p++;
if (*p == '\'') p++;
else { free(buf); free(argv); return -1; }
}
else if (*p == '"') {
/* double quote: with escape sequences */
p++;
while (*p != '\0' && *p != '"') {
if (*p == '\\' && p[1] != '\0') {
p++;
switch (*p) {
case '"': buf[bp++] = '"'; break;
case '\\': buf[bp++] = '\\'; break;
case 'n': buf[bp++] = '\n'; break;
case 't': buf[bp++] = '\t'; break;
case '$': buf[bp++] = '$'; break;
default: buf[bp++] = '\\'; buf[bp++] = *p; break;
}
p++;
}
else {
buf[bp++] = *p++;
}
}
if (*p == '"') p++;
else { free(buf); free(argv); return -1; }
}
else if (*p == '\\' && p[1] != '\0') {
/* backslash escape outside quotes */
p++;
buf[bp++] = *p++;
}
else {
buf[bp++] = *p++;
}
}
buf[bp++] = '\0';
}
argv[argc] = NULL;
result->argv = argv;
result->buf = buf;
result->argc = argc;
return 0;
}
/* ===== pipeline splitter ===== */
#define MAX_PIPELINE_STAGES 64
/*
** Split a command string on unquoted '|' into pipeline stages.
** Reuses the same quote-tracking logic as parse_argv().
** Returns 0 on success, -1 on error (empty stage, leading/trailing pipe).
** Writes stage pointers into stages[] and count into *nstages.
** Caller must free stages[0] (the backing buffer).
*/
static int split_pipeline (const char *cmd, char **stages, int *nstages) {
size_t len = strlen(cmd);
char *buf = (char *)malloc(len + 1);
int count = 0;
const char *p = cmd;
size_t bp = 0;
if (buf == NULL) return -1;
stages[count++] = buf;
while (*p != '\0') {
if (*p == '\'' ) {
/* single-quoted string: copy through including quotes */
buf[bp++] = *p++;
while (*p != '\0' && *p != '\'')
buf[bp++] = *p++;
if (*p == '\'') buf[bp++] = *p++;
}
else if (*p == '"') {
/* double-quoted string: copy through, respecting backslash */
buf[bp++] = *p++;
while (*p != '\0' && *p != '"') {
if (*p == '\\' && p[1] != '\0') {
buf[bp++] = *p++;
buf[bp++] = *p++;
} else {
buf[bp++] = *p++;
}
}
if (*p == '"') buf[bp++] = *p++;
}
else if (*p == '\\' && p[1] != '\0') {
/* backslash escape: copy both chars */
buf[bp++] = *p++;
buf[bp++] = *p++;
}
else if (*p == '|') {
/* unquoted pipe — split here */
buf[bp++] = '\0';
if (count >= MAX_PIPELINE_STAGES) { free(buf); return -1; }
stages[count++] = buf + bp;
p++;
}
else {
buf[bp++] = *p++;
}
}
buf[bp] = '\0';
/* validate: no empty stages (only for multi-stage pipelines) */
if (count > 1) {
for (int i = 0; i < count; i++) {
const char *s = stages[i];
/* skip whitespace */
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s == '\0') { free(buf); return -1; }
}
}
*nstages = count;
return 0;
}
/* ===== pipe reader ===== */
#define READ_CHUNK 4096
typedef struct {
char *data;
size_t len;
size_t cap;
} DynBuf;
static void dynbuf_init (DynBuf *b) {
b->data = NULL;
b->len = 0;
b->cap = 0;
}
static void dynbuf_free (DynBuf *b) {
free(b->data);
b->data = NULL;
b->len = b->cap = 0;
}
static int dynbuf_grow (DynBuf *b, size_t need) {
if (b->len + need > b->cap) {
size_t newcap = (b->cap == 0) ? READ_CHUNK : b->cap;
char *p;
while (newcap < b->len + need)
newcap *= 2;
p = (char *)realloc(b->data, newcap);
if (p == NULL) return -1;
b->data = p;
b->cap = newcap;
}
return 0;
}
/*
** Read from two fds simultaneously into DynBufs using select().
** Avoids deadlock when child fills one pipe buffer.
*/
static void read_pipes (int fd_out, int fd_err,
DynBuf *buf_out, DynBuf *buf_err) {
int nfds = (fd_out > fd_err ? fd_out : fd_err) + 1;
int open_out = 1, open_err = 1;
while (open_out || open_err) {
fd_set rfds;
int ret;
FD_ZERO(&rfds);
if (open_out) FD_SET(fd_out, &rfds);
if (open_err) FD_SET(fd_err, &rfds);
ret = select(nfds, &rfds, NULL, NULL, NULL);
if (ret < 0) {
if (errno == EINTR) continue;
break; /* unexpected error */
}
if (open_out && FD_ISSET(fd_out, &rfds)) {
ssize_t n;
if (dynbuf_grow(buf_out, READ_CHUNK) != 0) break;
n = read(fd_out, buf_out->data + buf_out->len, READ_CHUNK);
if (n > 0)
buf_out->len += (size_t)n;
else {
close(fd_out);
open_out = 0;
}
}
if (open_err && FD_ISSET(fd_err, &rfds)) {
ssize_t n;
if (dynbuf_grow(buf_err, READ_CHUNK) != 0) break;
n = read(fd_err, buf_err->data + buf_err->len, READ_CHUNK);
if (n > 0)
buf_err->len += (size_t)n;
else {
close(fd_err);
open_err = 0;
}
}
}
}
/* ===== pipeline execution ===== */
/*
** Build result table {code=N, stdout=S, stderr=S} on the Lua stack.
*/
static void push_result_table (lua_State *L, int code,
DynBuf *buf_out, DynBuf *buf_err) {
lua_createtable(L, 0, 3);
lua_pushinteger(L, code);
lua_setfield(L, -2, "code");
lua_pushlstring(L, buf_out->data ? buf_out->data : "", buf_out->len);
lua_setfield(L, -2, "stdout");
lua_pushlstring(L, buf_err->data ? buf_err->data : "", buf_err->len);
lua_setfield(L, -2, "stderr");
}
/*
** Execute a multi-stage pipeline.
** stages[0..nstages-1] are command strings. Each is parsed with parse_argv().
** Inter-stage pipes connect stdout→stdin. Only the last stage's stdout/stderr
** 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,
int interactive) {
ParsedArgs *pa = NULL;
int (*inter_pipes)[2] = NULL; /* inter_pipes[i] connects stage i → i+1 */
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_pipe, sa_old_int, sa_old_quit, sa_new;
/* parse all stages */
pa = (ParsedArgs *)calloc((size_t)nstages, sizeof(ParsedArgs));
if (pa == NULL) return luaL_error(L, "out of memory");
for (i = 0; i < nstages; i++) {
if (parse_argv(stages[i], &pa[i]) != 0) {
for (int j = 0; j < i; j++) free_argv(&pa[j]);
free(pa);
return luaL_error(L, "unterminated quote in pipeline stage %d", i + 1);
}
if (pa[i].argc == 0) {
for (int j = 0; j <= i; j++) free_argv(&pa[j]);
free(pa);
return luaL_error(L, "empty command in pipeline stage %d", i + 1);
}
}
/* allocate inter-stage pipes (nstages-1 of them) */
if (nstages > 1) {
inter_pipes = (int (*)[2])calloc((size_t)(nstages - 1), sizeof(int [2]));
if (inter_pipes == NULL) {
for (i = 0; i < nstages; i++) free_argv(&pa[i]);
free(pa);
return luaL_error(L, "out of memory");
}
for (i = 0; i < nstages - 1; i++) {
if (pipe(inter_pipes[i]) != 0) {
for (int j = 0; j < i; 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);
return luaL_error(L, "pipe() failed: %s", strerror(errno));
}
}
}
/* 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));
}
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));
}
}
/* 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_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) {
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_pipe, NULL);
if (interactive) {
sigaction(SIGINT, &sa_old_int, NULL);
sigaction(SIGQUIT, &sa_old_quit, NULL);
}
return luaL_error(L, "out of memory");
}
/* fork all stages */
for (i = 0; i < nstages; i++) {
pids[i] = fork();
if (pids[i] < 0) {
/* fork failed — kill already-forked children */
for (int j = 0; j < i; j++) {
kill(pids[j], SIGTERM);
waitpid(pids[j], NULL, 0);
}
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_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));
}
if (pids[i] == 0) {
/* === child process for stage i === */
int j;
/* 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) {
dup2(inter_pipes[i - 1][0], STDIN_FILENO);
}
/* set up stdout */
if (i < nstages - 1) {
dup2(inter_pipes[i][1], STDOUT_FILENO);
} 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) {
for (j = 0; j < nstages - 1; j++) {
close(inter_pipes[j][0]);
close(inter_pipes[j][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);
/* exec failed */
{
const char *err = strerror(errno);
size_t namelen = strlen(pa[i].argv[0]);
size_t errlen = strlen(err);
(void)write(STDERR_FILENO, pa[i].argv[0], namelen);
(void)write(STDERR_FILENO, ": ", 2);
(void)write(STDERR_FILENO, err, errlen);
(void)write(STDERR_FILENO, "\n", 1);
}
_exit(127);
}
}
/* === parent: close all write ends === */
if (inter_pipes) {
for (i = 0; i < nstages - 1; i++) {
close(inter_pipes[i][0]);
close(inter_pipes[i][1]);
}
free(inter_pipes);
}
/* free parsed args (children have already exec'd) */
for (i = 0; i < nstages; i++) free_argv(&pa[i]);
free(pa);
dynbuf_init(&buf_out);
dynbuf_init(&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++) {
int status;
waitpid(pids[i], &status, 0);
if (i == nstages - 1) {
if (WIFEXITED(status))
last_code = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
last_code = 128 + WTERMSIG(status);
else
last_code = -1;
}
}
free(pids);
/* 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);
dynbuf_free(&buf_out);
dynbuf_free(&buf_err);
return 1;
}
/* ===== luaB_command ===== */
int luaB_command (lua_State *L) {
const char *cmd = luaL_checkstring(L, 1);
char *stages[MAX_PIPELINE_STAGES];
int nstages;
ParsedArgs pa;
int out_pipe[2], err_pipe[2];
pid_t pid;
int status;
DynBuf buf_out, buf_err;
struct sigaction sa_old, sa_new;
/* 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 */
if (nstages > 1) {
int result = exec_pipeline(L, stages, nstages, 0);
free(stages[0]); /* free backing buffer */
return result;
}
/* single stage: free pipeline buffer and use original path */
free(stages[0]);
/* parse command into argv */
if (parse_argv(cmd, &pa) != 0)
return luaL_error(L, "unterminated quote in command");
/* empty command: return {code=0, stdout="", stderr=""} */
if (pa.argc == 0) {
free_argv(&pa);
lua_createtable(L, 0, 3);
lua_pushinteger(L, 0);
lua_setfield(L, -2, "code");
lua_pushliteral(L, "");
lua_setfield(L, -2, "stdout");
lua_pushliteral(L, "");
lua_setfield(L, -2, "stderr");
return 1;
}
/* create pipes */
if (pipe(out_pipe) != 0) {
free_argv(&pa);
return luaL_error(L, "pipe() failed: %s", strerror(errno));
}
if (pipe(err_pipe) != 0) {
close(out_pipe[0]); close(out_pipe[1]);
free_argv(&pa);
return luaL_error(L, "pipe() failed: %s", strerror(errno));
}
/* ignore SIGPIPE so parent doesn't crash if child exits early */
memset(&sa_new, 0, sizeof(sa_new));
sa_new.sa_handler = SIG_IGN;
sigemptyset(&sa_new.sa_mask);
sigaction(SIGPIPE, &sa_new, &sa_old);
pid = fork();
if (pid < 0) {
close(out_pipe[0]); close(out_pipe[1]);
close(err_pipe[0]); close(err_pipe[1]);
free_argv(&pa);
sigaction(SIGPIPE, &sa_old, NULL);
return luaL_error(L, "fork() failed: %s", strerror(errno));
}
if (pid == 0) {
/* child */
close(out_pipe[0]);
close(err_pipe[0]);
dup2(out_pipe[1], STDOUT_FILENO);
dup2(err_pipe[1], STDERR_FILENO);
close(out_pipe[1]);
close(err_pipe[1]);
/* restore default SIGPIPE for child */
sa_new.sa_handler = SIG_DFL;
sigaction(SIGPIPE, &sa_new, NULL);
execvp(pa.argv[0], pa.argv);
/* exec failed — write error to stderr and exit */
{
const char *err = strerror(errno);
size_t namelen = strlen(pa.argv[0]);
size_t errlen = strlen(err);
/* write "cmd: error\n" */
(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 */
close(out_pipe[1]);
close(err_pipe[1]);
free_argv(&pa);
dynbuf_init(&buf_out);
dynbuf_init(&buf_err);
read_pipes(out_pipe[0], err_pipe[0], &buf_out, &buf_err);
waitpid(pid, &status, 0);
/* restore old SIGPIPE handler */
sigaction(SIGPIPE, &sa_old, NULL);
/* build result table */
lua_createtable(L, 0, 3);
if (WIFEXITED(status))
lua_pushinteger(L, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
lua_pushinteger(L, 128 + WTERMSIG(status));
else
lua_pushinteger(L, -1);
lua_setfield(L, -2, "code");
lua_pushlstring(L, buf_out.data ? buf_out.data : "", buf_out.len);
lua_setfield(L, -2, "stdout");
lua_pushlstring(L, buf_err.data ? buf_err.data : "", buf_err.len);
lua_setfield(L, -2, "stderr");
dynbuf_free(&buf_out);
dynbuf_free(&buf_err);
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 */
}