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
|
||||
|
||||
**Status:** open
|
||||
**Status:** done
|
||||
|
||||
## 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 ===== */
|
||||
|
||||
int lushCmd_interactive (lua_State *L) {
|
||||
|
||||
4
lcmd.h
4
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
|
||||
|
||||
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.
|
||||
** 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);
|
||||
}
|
||||
|
||||
|
||||
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 */
|
||||
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 */
|
||||
|
||||
3
llex.h
3
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;
|
||||
|
||||
|
||||
|
||||
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);
|
||||
|
||||
/*
|
||||
** Parse a single interpolation fragment in a command: either ${expr}
|
||||
** or $NAME. Emits tostring(<value>), reads the continuation fragment,
|
||||
** Parse a single interpolation fragment in a command: ${expr}, $NAME,
|
||||
** or $(cmd). Emits tostring(<value>), 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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user