/* ** $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 #include "lua.h" #include "lauxlib.h" #include "lcmd.h" #include "lbuiltin.h" #include "lstring.h" /* ===== argv parser ===== */ #define PA_QUOTED 1 /* suppress glob/tilde expansion for this token */ typedef struct { char **argv; char *buf; int argc; int *flags; /* per-token flags (PA_QUOTED etc.) */ } ParsedArgs; static void free_argv (ParsedArgs *pa) { free(pa->buf); free(pa->argv); free(pa->flags); } /* ===== glob/tilde/brace expansion ===== */ /* ** Expand ~ at the start of a token to the home directory. ** Returns a malloc'd string (caller must free), or NULL on failure. ** If no tilde prefix, returns a strdup of the original. */ static char *expand_tilde (const char *token) { const char *home; size_t homelen, restlen; char *result; if (token[0] != '~') return strdup(token); /* only handle plain ~ and ~/path (not ~user for now) */ if (token[1] != '\0' && token[1] != '/') return strdup(token); home = getenv("HOME"); if (home == NULL) return strdup(token); homelen = strlen(home); restlen = strlen(token + 1); /* everything after ~ */ result = (char *)malloc(homelen + restlen + 1); if (result == NULL) return NULL; memcpy(result, home, homelen); memcpy(result + homelen, token + 1, restlen + 1); return result; } /* ** Brace expansion: expand {a,b,c} patterns into multiple strings. ** Handles prefix{a,b,c}suffix → prefixasuffix prefixbsuffix prefixcsuffix. ** Does NOT handle nested braces (kept literal). ** Writes results into out[] (caller provides), returns count. ** out[] entries are malloc'd strings. max_out limits array size. ** Returns 0 if no braces found (token unchanged). */ #define MAX_BRACE_RESULTS 256 static int expand_braces (const char *token, char **out, int max_out) { const char *open, *close, *p; size_t prefix_len, suffix_len; int depth, ncommas, count; const char *alts[MAX_BRACE_RESULTS]; size_t altlens[MAX_BRACE_RESULTS]; int nalts = 0; /* find first '{' */ open = strchr(token, '{'); if (open == NULL) return 0; /* find matching '}' at depth 0, counting commas */ depth = 1; ncommas = 0; for (p = open + 1; *p != '\0' && depth > 0; p++) { if (*p == '{') depth++; else if (*p == '}') depth--; else if (*p == ',' && depth == 1) ncommas++; } if (depth != 0 || ncommas == 0) return 0; /* no valid brace expression */ close = p - 1; /* points to '}' */ prefix_len = (size_t)(open - token); suffix_len = strlen(close + 1); /* split alternatives on ',' at depth 0 */ p = open + 1; alts[0] = p; for (; p < close; p++) { if (*p == '{') depth++; else if (*p == '}') depth--; else if (*p == ',' && depth == 0) { altlens[nalts] = (size_t)(p - alts[nalts]); nalts++; if (nalts >= MAX_BRACE_RESULTS) return 0; alts[nalts] = p + 1; } } altlens[nalts] = (size_t)(close - alts[nalts]); nalts++; /* generate prefix + alt + suffix for each alternative */ count = 0; for (int i = 0; i < nalts && count < max_out; i++) { size_t total = prefix_len + altlens[i] + suffix_len + 1; char *s = (char *)malloc(total); if (s == NULL) { for (int j = 0; j < count; j++) free(out[j]); return -1; } memcpy(s, token, prefix_len); memcpy(s + prefix_len, alts[i], altlens[i]); memcpy(s + prefix_len + altlens[i], close + 1, suffix_len + 1); out[count++] = s; } return count; } /* ** Growing list of tokens used during expansion. ** Each item is a separately malloc'd string. */ typedef struct { char **argv; int *flags; int argc; int capacity; } TokenList; static int tklist_init (TokenList *tl, int capacity) { tl->argv = (char **)malloc((size_t)capacity * sizeof(char *)); tl->flags = (int *)calloc((size_t)capacity, sizeof(int)); tl->argc = 0; tl->capacity = capacity; if (tl->argv == NULL || tl->flags == NULL) { free(tl->argv); free(tl->flags); return -1; } return 0; } static void tklist_free (TokenList *tl) { int i; for (i = 0; i < tl->argc; i++) free(tl->argv[i]); free(tl->argv); free(tl->flags); } /* ** Add a token to the list. Takes ownership of 's' on success. ** Returns 0 on success, -1 on allocation failure (caller still owns 's'). */ static int tklist_add (TokenList *tl, char *s, int flag) { if (tl->argc >= tl->capacity) { int newcap = tl->capacity * 2; char **a = (char **)realloc(tl->argv, (size_t)newcap * sizeof(char *)); int *f = (int *)realloc(tl->flags, (size_t)newcap * sizeof(int)); if (a == NULL || f == NULL) { /* if one succeeded, keep the original pointer valid */ if (a) tl->argv = a; if (f) tl->flags = f; return -1; } tl->argv = a; tl->flags = f; tl->capacity = newcap; } tl->argv[tl->argc] = s; tl->flags[tl->argc] = flag; tl->argc++; return 0; } /* ** Glob a single token and append results to 'tl'. ** If the token has no metacharacters, it is added as-is (takes ownership). ** Otherwise, glob results are added and 'token' is freed. ** Returns 0 on success, -1 on failure (token is freed either way). */ static int glob_token (TokenList *tl, char *token) { const char *s; int has_meta = 0; for (s = token; *s != '\0'; s++) { if (*s == '*' || *s == '?' || *s == '[') { has_meta = 1; break; } } if (!has_meta) return tklist_add(tl, token, 0); /* no glob needed */ { glob_t g; int ret = glob(token, GLOB_NOCHECK, NULL, &g); free(token); if (ret != 0) return 0; /* GLOB_NOCHECK means this shouldn't happen, but be safe */ { size_t k; for (k = 0; k < g.gl_pathc; k++) { char *dup = strdup(g.gl_pathv[k]); if (dup == NULL || tklist_add(tl, dup, 0) != 0) { free(dup); globfree(&g); return -1; } } } globfree(&g); return 0; } } /* ** Expand unquoted tokens: tilde → brace → glob. ** Quoted tokens (PA_QUOTED) are passed through unchanged. ** Replaces pa->argv, pa->buf, and pa->flags in place. ** Returns 0 on success, -1 on allocation failure. */ static int expand_argv (ParsedArgs *pa) { int i; TokenList tl; size_t total_buflen = 0; char *new_buf; size_t bp = 0; if (tklist_init(&tl, pa->argc + 16) != 0) return -1; for (i = 0; i < pa->argc; i++) { if (pa->flags[i] & PA_QUOTED) { char *dup = strdup(pa->argv[i]); if (dup == NULL || tklist_add(&tl, dup, PA_QUOTED) != 0) { free(dup); goto fail; } } else { char *tilded; char *brace_results[MAX_BRACE_RESULTS]; int nbrace; /* step 1: tilde expansion */ tilded = expand_tilde(pa->argv[i]); if (tilded == NULL) goto fail; /* step 2: brace expansion */ nbrace = expand_braces(tilded, brace_results, MAX_BRACE_RESULTS); if (nbrace < 0) { free(tilded); goto fail; } if (nbrace == 0) { /* no braces — glob the tilde-expanded token */ if (glob_token(&tl, tilded) != 0) goto fail; } else { int j; free(tilded); /* glob each brace alternative */ for (j = 0; j < nbrace; j++) { if (glob_token(&tl, brace_results[j]) != 0) { /* free remaining brace results */ for (j++; j < nbrace; j++) free(brace_results[j]); goto fail; } } } } } /* pack all tokens into a single contiguous buffer */ for (i = 0; i < tl.argc; i++) total_buflen += strlen(tl.argv[i]) + 1; new_buf = (char *)malloc(total_buflen > 0 ? total_buflen : 1); if (new_buf == NULL) goto fail; for (i = 0; i < tl.argc; i++) { char *old = tl.argv[i]; size_t slen = strlen(old); memcpy(new_buf + bp, old, slen + 1); tl.argv[i] = new_buf + bp; bp += slen + 1; free(old); } /* replace old arrays */ free(pa->argv); free(pa->buf); free(pa->flags); pa->argv = (char **)realloc(tl.argv, ((size_t)tl.argc + 1) * sizeof(char *)); if (pa->argv == NULL) pa->argv = tl.argv; pa->argv[tl.argc] = NULL; pa->buf = new_buf; pa->flags = tl.flags; pa->argc = tl.argc; return 0; fail: tklist_free(&tl); return -1; } /* ** 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 *flags = (int *)calloc((size_t)capacity, sizeof(int)); int argc = 0; size_t bp = 0; /* position in buf */ const char *p = cmd; if (buf == NULL || argv == NULL || flags == NULL) { free(buf); free(argv); free(flags); return -1; } while (*p != '\0') { int quoted = 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 *)); flags = (int *)realloc(flags, (size_t)capacity * sizeof(int)); if (argv == NULL || flags == NULL) { free(buf); free(argv); free(flags); 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 */ quoted = 1; p++; while (*p != '\0' && *p != '\'') buf[bp++] = *p++; if (*p == '\'') p++; else { free(buf); free(argv); free(flags); return -1; } } else if (*p == '"') { /* double quote: with escape sequences */ quoted = 1; 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); free(flags); return -1; } } else if (*p == '\\' && p[1] != '\0') { /* backslash escape outside quotes */ quoted = 1; p++; buf[bp++] = *p++; } else { buf[bp++] = *p++; } } buf[bp++] = '\0'; flags[argc] = quoted ? PA_QUOTED : 0; argc++; } argv[argc] = NULL; result->argv = argv; result->buf = buf; result->argc = argc; result->flags = flags; /* expand globs and tildes on unquoted tokens */ if (expand_argv(result) != 0) { free(buf); free(argv); free(flags); return -1; } 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"); } /* ** Extract exit code from wait status. */ static int exit_code_from_status (int status) { if (WIFEXITED(status)) return WEXITSTATUS(status); else if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); return -1; } /* ** Write "name: strerror(errno)\n" to stderr after exec failure. ** Only called in forked child processes. */ static void exec_failed (const char *name) { const char *err = strerror(errno); (void)write(STDERR_FILENO, name, strlen(name)); (void)write(STDERR_FILENO, ": ", 2); (void)write(STDERR_FILENO, err, strlen(err)); (void)write(STDERR_FILENO, "\n", 1); } /* ** Look up a function in lush[table][key]. ** On success, pushes the function and returns 1. ** On failure, cleans the stack and returns 0. */ static int lookup_lush_func (lua_State *L, const char *table, const char *key) { lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH); if (!lua_istable(L, -1)) { lua_pop(L, 1); return 0; } if (lua_getfield(L, -1, table) != LUA_TTABLE) { lua_pop(L, 2); return 0; } if (lua_getfield(L, -1, key) != LUA_TFUNCTION) { lua_pop(L, 3); return 0; } lua_remove(L, -2); /* remove subtable */ lua_remove(L, -2); /* remove lush table */ return 1; } /* ===== user command dispatch (runs in forked child) ===== */ /* ** Look up argv[0] in lush.commands. If found, call the function with ** all argv strings, extract an exit code, and _exit(). This must only ** be called inside a fork()ed child process. ** Returns 0 if not a user command (caller should fall through to execvp). */ static int exec_user_command (lua_State *L, ParsedArgs *pa) { int i, code = 0; if (!lookup_lush_func(L, "commands", pa->argv[0])) return 0; for (i = 0; i < pa->argc; i++) lua_pushstring(L, pa->argv[i]); if (lua_pcall(L, pa->argc, 1, 0) != LUA_OK) { /* write error to stderr and exit with failure */ const char *err = lua_tostring(L, -1); if (err) { (void)write(STDERR_FILENO, err, strlen(err)); (void)write(STDERR_FILENO, "\n", 1); } _exit(1); } /* extract exit code from return value */ if (lua_isinteger(L, -1)) { code = (int)lua_tointeger(L, -1); } else if (lua_istable(L, -1)) { if (lua_getfield(L, -1, "code") == LUA_TNUMBER) code = (int)lua_tointeger(L, -1); lua_pop(L, 1); } fflush(stdout); fflush(stderr); _exit(code); return 1; /* unreachable */ } /* ** 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]); } exec_user_command(L, &pa[i]); execvp(pa[i].argv[0], pa[i].argv); exec_failed(pa[i].argv[0]); _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) last_code = exit_code_from_status(status); } 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; } /* ===== builtin dispatch ===== */ /* ** Check if argv[0] is a shell builtin. If so, call it and push the ** result table. Returns 1 if handled, 0 if not a builtin. */ static int try_builtin (lua_State *L, ParsedArgs *pa) { int i; if (!lookup_lush_func(L, "builtins", pa->argv[0])) return 0; for (i = 0; i < pa->argc; i++) lua_pushstring(L, pa->argv[i]); lua_call(L, pa->argc, 1); return 1; } /* ===== alias expansion ===== */ /* ** Extract the first whitespace-delimited word from cmd, look it up in ** lush.aliases. If found, replace the command string on the Lua stack ** (position 1) with alias_value + remaining_text, and update *cmd. ** Expansion happens on the raw string, before pipeline splitting, ** so alias values may contain pipes. ** Returns 1 if expanded, 0 if no alias. */ static int expand_alias (lua_State *L, const char **cmd) { const char *s = *cmd; const char *rest; size_t wordlen; const char *alias; size_t alias_len, rest_len, total; char *expanded; /* skip leading whitespace */ while (*s == ' ' || *s == '\t') s++; if (*s == '\0') return 0; /* find end of first word */ rest = s; while (*rest && *rest != ' ' && *rest != '\t') rest++; wordlen = (size_t)(rest - s); /* look up in lush.aliases */ lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH); lua_getfield(L, -1, "aliases"); lua_pushlstring(L, s, wordlen); lua_gettable(L, -2); if (!lua_isstring(L, -1)) { lua_pop(L, 3); return 0; } alias = lua_tolstring(L, -1, &alias_len); rest_len = strlen(rest); /* includes leading space if any */ total = alias_len + rest_len; expanded = (char *)malloc(total + 1); if (expanded == NULL) { lua_pop(L, 3); return 0; } memcpy(expanded, alias, alias_len); memcpy(expanded + alias_len, rest, rest_len); expanded[total] = '\0'; lua_pop(L, 3); /* replace command on Lua stack */ lua_pushstring(L, expanded); lua_replace(L, 1); free(expanded); *cmd = lua_tostring(L, 1); return 1; } /* ===== single-command fork/exec/wait ===== */ /* ** Fork, exec a single command, wait, and push a result table. ** If capture is true, stdout/stderr are captured via pipes. ** If capture is false, the child inherits the terminal. */ static void fork_exec_single (lua_State *L, ParsedArgs *pa, int capture) { int out_pipe[2], err_pipe[2]; pid_t pid; int status; DynBuf buf_out, buf_err; struct sigaction sa_old_pipe, sa_old_int, sa_old_quit, sa_new; if (capture) { if (pipe(out_pipe) != 0) luaL_error(L, "pipe() failed: %s", strerror(errno)); if (pipe(err_pipe) != 0) { close(out_pipe[0]); close(out_pipe[1]); luaL_error(L, "pipe() failed: %s", strerror(errno)); } } 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 (!capture) { sigaction(SIGINT, &sa_new, &sa_old_int); sigaction(SIGQUIT, &sa_new, &sa_old_quit); } pid = fork(); if (pid < 0) { if (capture) { close(out_pipe[0]); close(out_pipe[1]); close(err_pipe[0]); close(err_pipe[1]); } sigaction(SIGPIPE, &sa_old_pipe, NULL); if (!capture) { sigaction(SIGINT, &sa_old_int, NULL); sigaction(SIGQUIT, &sa_old_quit, NULL); } luaL_error(L, "fork() failed: %s", strerror(errno)); } if (pid == 0) { /* child */ sa_new.sa_handler = SIG_DFL; sigaction(SIGPIPE, &sa_new, NULL); if (!capture) { sigaction(SIGINT, &sa_new, NULL); sigaction(SIGQUIT, &sa_new, NULL); } if (capture) { 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]); } exec_user_command(L, pa); execvp(pa->argv[0], pa->argv); exec_failed(pa->argv[0]); _exit(127); } /* parent */ dynbuf_init(&buf_out); dynbuf_init(&buf_err); if (capture) { close(out_pipe[1]); close(err_pipe[1]); read_pipes(out_pipe[0], err_pipe[0], &buf_out, &buf_err); } waitpid(pid, &status, 0); sigaction(SIGPIPE, &sa_old_pipe, NULL); if (!capture) { sigaction(SIGINT, &sa_old_int, NULL); sigaction(SIGQUIT, &sa_old_quit, NULL); } push_result_table(L, exit_code_from_status(status), &buf_out, &buf_err); dynbuf_free(&buf_out); dynbuf_free(&buf_err); } /* ===== lushCmd_command ===== */ int lushCmd_command (lua_State *L) { const char *cmd = luaL_checkstring(L, 1); char *stages[MAX_PIPELINE_STAGES]; int nstages; ParsedArgs pa; DynBuf empty_out, empty_err; /* expand alias before pipeline splitting */ expand_alias(L, &cmd); /* 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); dynbuf_init(&empty_out); dynbuf_init(&empty_err); push_result_table(L, 0, &empty_out, &empty_err); return 1; } /* check for shell builtin */ if (try_builtin(L, &pa)) { free_argv(&pa); return 1; } fork_exec_single(L, &pa, 1); free_argv(&pa); return 1; } /* ===== lushCmd_subcmd ===== */ /* ** Subcommand substitution: run a command, return stdout with trailing ** newlines stripped. Used for $(cmd) inside command strings. */ int lushCmd_subcmd (lua_State *L) { size_t len; const char *s; /* run the command — pushes {code, stdout, stderr} table */ lushCmd_command(L); /* extract .stdout from result table */ lua_getfield(L, -1, "stdout"); lua_remove(L, -2); /* remove the table, keep stdout string */ /* strip trailing newlines */ s = lua_tolstring(L, -1, &len); while (len > 0 && s[len - 1] == '\n') len--; lua_pushlstring(L, s, len); lua_remove(L, -2); /* remove original stdout string */ return 1; } /* ===== lushCmd_interactive ===== */ int lushCmd_interactive (lua_State *L) { const char *cmd = luaL_checkstring(L, 1); char *stages[MAX_PIPELINE_STAGES]; int nstages; ParsedArgs pa; DynBuf empty_out, empty_err; /* expand alias before pipeline splitting */ expand_alias(L, &cmd); /* 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]); goto set_and_return; } /* 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(&empty_out); dynbuf_init(&empty_err); push_result_table(L, 0, &empty_out, &empty_err); goto set_and_return; } /* check for shell builtin */ if (try_builtin(L, &pa)) { free_argv(&pa); goto set_and_return; } fork_exec_single(L, &pa, 0); free_argv(&pa); set_and_return: lua_pushvalue(L, -1); /* duplicate result table */ lua_setglobal(L, "_"); /* pop one copy into _ */ return 1; /* return the other */ } /* ===== lushCmd_getenv ===== */ int lushCmd_getenv (lua_State *L) { const char *name = luaL_checkstring(L, 1); const char *val = getenv(name); if (val == NULL) lua_pushnil(L); else lua_pushstring(L, val); return 1; } /* ===== lushCmd_setenv ===== */ int lushCmd_setenv (lua_State *L) { const char *name = luaL_checkstring(L, 1); if (lua_isnoneornil(L, 2)) unsetenv(name); else { const char *val = luaL_tolstring(L, 2, NULL); setenv(name, val, 1); } return 0; } /* ===== lush standard library ===== */ TString *lushname[LUSH_OP_COUNT]; static const luaL_Reg lushlib[] = { {"command", lushCmd_command}, {"interactive", lushCmd_interactive}, {"getenv", lushCmd_getenv}, {"setenv", lushCmd_setenv}, {"subcmd", lushCmd_subcmd}, {NULL, NULL} }; LUAMOD_API int luaopen_lush (lua_State *L) { luaL_newlib(L, lushlib); /* create aliases subtable */ lua_createtable(L, 0, 4); lua_setfield(L, -2, "aliases"); /* create commands subtable for user-defined commands */ lua_createtable(L, 0, 4); lua_setfield(L, -2, "commands"); /* intern function name strings for OP_LUSH VM access */ lushname[LUSH_OP_COMMAND] = luaS_new(L, "command"); lushname[LUSH_OP_INTERACTIVE] = luaS_new(L, "interactive"); lushname[LUSH_OP_GETENV] = luaS_new(L, "getenv"); lushname[LUSH_OP_SETENV] = luaS_new(L, "setenv"); lushname[LUSH_OP_SUBCMD] = luaS_new(L, "subcmd"); /* store in registry for OP_LUSH access */ lua_pushvalue(L, -1); lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH); luaopen_builtins(L); /* register builtins in lush table */ return 1; }