diff --git a/issues/27-subcommand-syntax.md b/issues/27-subcommand-syntax.md index f398c44f..c827a65c 100644 --- a/issues/27-subcommand-syntax.md +++ b/issues/27-subcommand-syntax.md @@ -1,6 +1,6 @@ # 27 — Subcommand syntax in commands -**Status:** open +**Status:** done ## Problem diff --git a/lcmd.c b/lcmd.c index 7870d6ab..535d8de4 100644 --- a/lcmd.c +++ b/lcmd.c @@ -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 ===== */ int lushCmd_interactive (lua_State *L) { diff --git a/lcmd.h b/lcmd.h index e5b8fb48..c922346c 100644 --- a/lcmd.h +++ b/lcmd.h @@ -14,11 +14,13 @@ #define LUSH_OP_INTERACTIVE 1 #define LUSH_OP_GETENV 2 #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_interactive (lua_State *L); int lushCmd_getenv (lua_State *L); int lushCmd_setenv (lua_State *L); +int lushCmd_subcmd (lua_State *L); #endif diff --git a/linit.c b/linit.c index 04e56fe7..209c9a65 100644 --- a/linit.c +++ b/linit.c @@ -45,7 +45,7 @@ static const luaL_Reg stdlibs[] = { /* ** 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). */ 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_pushcfunction(L, lushCmd_setenv); 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); } diff --git a/llex.c b/llex.c index 1bc26926..6a8d9e6a 100644 --- a/llex.c +++ b/llex.c @@ -193,6 +193,7 @@ void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source, so they cannot be collected */ ls->cmd_mode = 0; ls->cmd_envvar = NULL; + ls->cmd_subcmd = 0; ls->envn = luaS_newliteral(L, LUA_ENV); /* get env string */ ls->brkn = luaS_newliteral(L, "break"); /* get "break" string */ #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 */ 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)) { /* $NAME envvar expansion */ seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff), @@ -532,6 +542,17 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) { } 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 '`': { if (interactive) { /* backtick is literal in interactive mode */ diff --git a/llex.h b/llex.h index 868622ce..b1df6756 100644 --- a/llex.h +++ b/llex.h @@ -81,8 +81,9 @@ typedef struct LexState { TString *brkn; /* "break" name (used as a label) */ TString *glbn; /* "global" name (when not a reserved word) */ 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 cmd_subcmd; /* 1 when current interpolation is $() subcommand */ } LexState; diff --git a/lparser.c b/lparser.c index 0fefaeaf..f27c1642 100644 --- a/lparser.c +++ b/lparser.c @@ -556,23 +556,59 @@ static void singlevar (LexState *ls, expdesc *var) { static void codeenvget (LexState *ls, expdesc *v, TString *name); /* -** Parse a single interpolation fragment in a command: either ${expr} -** or $NAME. Emits tostring(), reads the continuation fragment, +** Parse a single interpolation fragment in a command: ${expr}, $NAME, +** or $(cmd). Emits tostring(), reads the continuation fragment, ** and pushes it as a string constant. ** For ${expr}: advances past the command token, parses expr, checks '}'. ** For $NAME: the identifier was already collected by the lexer into ** 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) { FuncState *fs = ls->fs; expdesc interp, tostrfn; int tostr_base; TString *envname = ls->cmd_envvar; + lu_byte subcmd = ls->cmd_subcmd; ls->cmd_envvar = NULL; + ls->cmd_subcmd = 0; buildglobal(ls, tsname, &tostrfn); luaK_exp2nextreg(fs, &tostrfn); 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) */ codeenvget(ls, &interp, envname); } diff --git a/testes/lush/interpolation.lua b/testes/lush/interpolation.lua index 90aa6db8..089b8144 100644 --- a/testes/lush/interpolation.lua +++ b/testes/lush/interpolation.lua @@ -109,4 +109,54 @@ do assert(r.stdout == "$HOME\n", "got: " .. r.stdout) 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"