/* ** $Id: lcmd.c $ ** Command execution for backtick syntax ** See Copyright Notice in lua.h */ #define lcmd_c #define LUA_LIB #include "lprefix.h" #include #include #include #include #include #include #include #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) { 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 */ pid_t *pids = NULL; int i, last_code = -1; DynBuf buf_out, buf_err; struct sigaction sa_old, 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 */ 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 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); /* 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 (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); 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); } 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); return luaL_error(L, "fork() failed: %s", strerror(errno)); } if (pids[i] == 0) { /* === child process for stage i === */ int j; /* restore default SIGPIPE */ sa_new.sa_handler = SIG_DFL; sigaction(SIGPIPE, &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 { /* last stage: capture stdout and stderr */ dup2(out_pipe[1], STDOUT_FILENO); dup2(err_pipe[1], STDERR_FILENO); } /* 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]); } } 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); } 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); /* 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 SIGPIPE */ sigaction(SIGPIPE, &sa_old, 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); 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; }