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:
@@ -1,6 +1,6 @@
|
|||||||
# 27 — Subcommand syntax in commands
|
# 27 — Subcommand syntax in commands
|
||||||
|
|
||||||
**Status:** open
|
**Status:** done
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
|
|||||||
24
lcmd.c
24
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 ===== */
|
/* ===== lushCmd_interactive ===== */
|
||||||
|
|
||||||
int lushCmd_interactive (lua_State *L) {
|
int lushCmd_interactive (lua_State *L) {
|
||||||
|
|||||||
4
lcmd.h
4
lcmd.h
@@ -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
|
||||||
|
|||||||
4
linit.c
4
linit.c
@@ -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
21
llex.c
@@ -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
3
llex.h
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
42
lparser.c
42
lparser.c
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user