Implement $(cmd) subcommand syntax in commands (issue #27)

Add $() for inline subcommand substitution that runs a command and
inserts its stdout (trailing newlines stripped) into the outer command
string. Supports nesting, and mixing with ${expr} and $NAME.
This commit is contained in:
Cormac Shannon
2026-03-18 09:29:51 +00:00
parent 42baabde34
commit 1766e40a68
8 changed files with 143 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
# 27 — Subcommand syntax in commands # 27 — Subcommand syntax in commands
**Status:** open **Status:** done
## Problem ## Problem

24
lcmd.c
View File

@@ -1039,6 +1039,30 @@ int lushCmd_command (lua_State *L) {
} }
/* ===== 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 ===== */ /* ===== lushCmd_interactive ===== */
int lushCmd_interactive (lua_State *L) { int lushCmd_interactive (lua_State *L) {

4
lcmd.h
View File

@@ -14,11 +14,13 @@
#define LUSH_OP_INTERACTIVE 1 #define LUSH_OP_INTERACTIVE 1
#define LUSH_OP_GETENV 2 #define LUSH_OP_GETENV 2
#define LUSH_OP_SETENV 3 #define LUSH_OP_SETENV 3
#define LUSH_OP_COUNT 4 #define LUSH_OP_SUBCMD 4
#define LUSH_OP_COUNT 5
int lushCmd_command (lua_State *L); int lushCmd_command (lua_State *L);
int lushCmd_interactive (lua_State *L); int lushCmd_interactive (lua_State *L);
int lushCmd_getenv (lua_State *L); int lushCmd_getenv (lua_State *L);
int lushCmd_setenv (lua_State *L); int lushCmd_setenv (lua_State *L);
int lushCmd_subcmd (lua_State *L);
#endif #endif

View File

@@ -45,7 +45,7 @@ static const luaL_Reg stdlibs[] = {
/* /*
** Store shell functions in a registry table at LUA_RIDX_LUSH. ** Store shell functions in a registry table at LUA_RIDX_LUSH.
** Integer keys 1..4 hold the C functions (accessed by OP_LUSH). ** Integer keys 1..5 hold the C functions (accessed by OP_LUSH).
** String key "builtins" holds the builtins table (set by luaopen_builtins). ** String key "builtins" holds the builtins table (set by luaopen_builtins).
*/ */
static void opencommand (lua_State *L) { static void opencommand (lua_State *L) {
@@ -58,6 +58,8 @@ static void opencommand (lua_State *L) {
lua_rawseti(L, -2, LUSH_OP_GETENV + 1); lua_rawseti(L, -2, LUSH_OP_GETENV + 1);
lua_pushcfunction(L, lushCmd_setenv); lua_pushcfunction(L, lushCmd_setenv);
lua_rawseti(L, -2, LUSH_OP_SETENV + 1); lua_rawseti(L, -2, LUSH_OP_SETENV + 1);
lua_pushcfunction(L, lushCmd_subcmd);
lua_rawseti(L, -2, LUSH_OP_SUBCMD + 1);
lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH); lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
} }

21
llex.c
View File

@@ -193,6 +193,7 @@ void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
so they cannot be collected */ so they cannot be collected */
ls->cmd_mode = 0; ls->cmd_mode = 0;
ls->cmd_envvar = NULL; ls->cmd_envvar = NULL;
ls->cmd_subcmd = 0;
ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */ ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */
ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */ ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */
#if defined(LUA_COMPAT_GLOBAL) #if defined(LUA_COMPAT_GLOBAL)
@@ -516,6 +517,15 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
ls->saved_cmd_mode = ls->cmd_mode; /* save for readcommandcont */ ls->saved_cmd_mode = ls->cmd_mode; /* save for readcommandcont */
return interactive ? TK_INTERACTIVE : TK_COMMAND; return interactive ? TK_INTERACTIVE : TK_COMMAND;
} }
else if (ls->current == '(') {
next(ls); /* skip '(' */
/* $() subcommand interpolation */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->saved_cmd_mode = ls->cmd_mode;
ls->cmd_subcmd = 1;
return interactive ? TK_INTERACTIVE : TK_COMMAND;
}
else if (lislalpha(ls->current)) { else if (lislalpha(ls->current)) {
/* $NAME envvar expansion */ /* $NAME envvar expansion */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
@@ -532,6 +542,17 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
} }
break; break;
} }
case ')': {
if (ls->cmd_mode == 3) {
next(ls); /* skip closing ')' */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->cmd_mode = 0;
return TK_COMMAND;
}
save_and_next(ls); /* literal ')' in non-subcommand mode */
break;
}
case '`': { case '`': {
if (interactive) { if (interactive) {
/* backtick is literal in interactive mode */ /* backtick is literal in interactive mode */

3
llex.h
View File

@@ -81,8 +81,9 @@ typedef struct LexState {
TString *brkn; /* "break" name (used as a label) */ TString *brkn; /* "break" name (used as a label) */
TString *glbn; /* "global" name (when not a reserved word) */ TString *glbn; /* "global" name (when not a reserved word) */
TString *cmd_envvar; /* envvar name for $NAME in commands */ TString *cmd_envvar; /* envvar name for $NAME in commands */
lu_byte cmd_mode; /* 0=normal, 1=backtick command, 2=interactive command */ lu_byte cmd_mode; /* 0=normal, 1=backtick, 2=interactive, 3=subcommand */
lu_byte saved_cmd_mode; /* cmd_mode to restore after interpolation */ lu_byte saved_cmd_mode; /* cmd_mode to restore after interpolation */
lu_byte cmd_subcmd; /* 1 when current interpolation is $() subcommand */
} LexState; } LexState;

View File

@@ -556,23 +556,59 @@ static void singlevar (LexState *ls, expdesc *var) {
static void codeenvget (LexState *ls, expdesc *v, TString *name); static void codeenvget (LexState *ls, expdesc *v, TString *name);
/* /*
** Parse a single interpolation fragment in a command: either ${expr} ** Parse a single interpolation fragment in a command: ${expr}, $NAME,
** or $NAME. Emits tostring(<value>), reads the continuation fragment, ** or $(cmd). Emits tostring(<value>), reads the continuation fragment,
** and pushes it as a string constant. ** and pushes it as a string constant.
** For ${expr}: advances past the command token, parses expr, checks '}'. ** For ${expr}: advances past the command token, parses expr, checks '}'.
** For $NAME: the identifier was already collected by the lexer into ** For $NAME: the identifier was already collected by the lexer into
** cmd_envvar — no tokens to consume, just emit getenv(name). ** cmd_envvar — no tokens to consume, just emit getenv(name).
** For $(cmd): enters subcommand mode (cmd_mode=3), parses the inner
** command with its own interpolation loop, emits subcmd().
*/ */
static void parseinterp (LexState *ls, TString *tsname, int *nconcat) { static void parseinterp (LexState *ls, TString *tsname, int *nconcat) {
FuncState *fs = ls->fs; FuncState *fs = ls->fs;
expdesc interp, tostrfn; expdesc interp, tostrfn;
int tostr_base; int tostr_base;
TString *envname = ls->cmd_envvar; TString *envname = ls->cmd_envvar;
lu_byte subcmd = ls->cmd_subcmd;
ls->cmd_envvar = NULL; ls->cmd_envvar = NULL;
ls->cmd_subcmd = 0;
buildglobal(ls, tsname, &tostrfn); buildglobal(ls, tsname, &tostrfn);
luaK_exp2nextreg(fs, &tostrfn); luaK_exp2nextreg(fs, &tostrfn);
tostr_base = tostrfn.u.info; tostr_base = tostrfn.u.info;
if (envname != NULL) { if (subcmd) {
/* $(cmd) — parse inner command as subcommand */
expdesc subfunc, cmdstr;
int subbase, subnconcat = 0;
lu_byte outer_saved = ls->saved_cmd_mode;
TString *sub_tsname = NULL;
codelushfunc(fs, LUSH_OP_SUBCMD, &subfunc);
subbase = subfunc.u.info;
/* enter subcommand lexing mode (terminated by ')') */
ls->saved_cmd_mode = 3;
luaX_readcommandcont(ls); /* read first fragment of inner command */
codestring(&cmdstr, ls->t.seminfo.ts);
luaK_exp2nextreg(fs, &cmdstr);
subnconcat++;
/* inner interpolation loop */
while (ls->cmd_mode != 0) {
if (sub_tsname == NULL)
sub_tsname = luaX_newstring(ls, "tostring", 8);
parseinterp(ls, sub_tsname, &subnconcat);
}
/* concatenate inner fragments if needed */
if (subnconcat > 1) {
int first = subbase + 1;
luaK_codeABC(fs, OP_CONCAT, first, subnconcat, 0);
fs->freereg = cast_byte(first + 1);
}
/* call subcmd(inner_cmd_string) */
init_exp(&interp, VCALL, luaK_codeABC(fs, OP_CALL, subbase, 2, 2));
fs->freereg = cast_byte(subbase + 1);
/* restore outer command context */
ls->saved_cmd_mode = outer_saved;
}
else if (envname != NULL) {
/* $NAME — emit getenv(name) */ /* $NAME — emit getenv(name) */
codeenvget(ls, &interp, envname); codeenvget(ls, &interp, envname);
} }

View File

@@ -109,4 +109,54 @@ do
assert(r.stdout == "$HOME\n", "got: " .. r.stdout) assert(r.stdout == "$HOME\n", "got: " .. r.stdout)
end end
-- $(cmd) subcommand: basic
do
local r = `echo $(echo hello)`
assert(r.stdout == "hello\n", "got: " .. r.stdout)
end
-- $(cmd) strips trailing newline from subcommand output
do
local r = `echo -$(echo world)-`
assert(r.stdout == "-world-\n", "got: " .. r.stdout)
end
-- $(cmd) multiple subcommands
do
local r = `echo $(echo aaa) $(echo bbb)`
assert(r.stdout == "aaa bbb\n", "got: " .. r.stdout)
end
-- $(cmd) nested subcommand
do
local r = `echo $(echo $(echo deep))`
assert(r.stdout == "deep\n", "got: " .. r.stdout)
end
-- $(cmd) with ${expr} inside subcommand
do
local r = `echo $(echo ${1+1})`
assert(r.stdout == "2\n", "got: " .. r.stdout)
end
-- $(cmd) with $NAME inside subcommand
do
local r = `echo $(echo $USER)`
assert(r.stdout == $USER .. "\n", "got: " .. r.stdout)
end
-- $(cmd) mixed with $NAME and ${expr}
do
local x = 42
local r = `echo $(echo sub) $USER ${x}`
local expected = "sub " .. $USER .. " 42\n"
assert(r.stdout == expected, "got: " .. r.stdout)
end
-- escaped \$(cmd) remains literal
do
local r = `echo \$(pwd)`
assert(r.stdout == "$(pwd)\n", "got: " .. r.stdout)
end
print "OK" print "OK"