Add command execution runtime (lcmd.c, lcmd.h)
Implements luaB_command: fork/execvp with pipe-captured stdout/stderr, argv parsing with quoting/escaping, and non-blocking pipe reading via select(). Backs the __command() calls emitted by the backtick parser. Resolves #03 and #04.
This commit is contained in:
340
lcmd.c
Normal file
340
lcmd.c
Normal file
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
** $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;
|
||||
}
|
||||
|
||||
|
||||
/* ===== 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===== luaB_command ===== */
|
||||
|
||||
int luaB_command (lua_State *L) {
|
||||
const char *cmd = luaL_checkstring(L, 1);
|
||||
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;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user