Implement prefix-based interactive commands via ! (issue #14)

Add !command syntax that runs commands with inherited terminal (no
capture). The lexer treats ! as a statement-starting token, reading
to end-of-line with the same interpolation/escape/pipe support as
backtick commands. The C function sets _ = {code=N, stdout="", stderr=""}.
This commit is contained in:
Cormac Shannon
2026-03-02 22:06:29 +00:00
parent 75098da240
commit db858b3f68
9 changed files with 399 additions and 47 deletions

36
llex.c
View File

@@ -50,7 +50,8 @@ static const char *const luaX_tokens [] = {
"//", "..", "...", "==", ">=", "<=", "~=",
"<<", ">>", "::", "<eof>",
"<number>", "<integer>", "<name>", "<string>",
"<command>", "<command_interp>", "<envvar>"
"<command>", "<command_interp>", "<envvar>",
"<interactive>", "<interactive_interp>"
};
@@ -190,7 +191,7 @@ void luaX_setinput (lua_State *L, LexState *ls, ZIO *z, TString *source,
ls->source = source;
/* all three strings here ("_ENV", "break", "global") were fixed,
so they cannot be collected */
ls->in_command = 0;
ls->cmd_mode = 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)
@@ -478,14 +479,29 @@ static void read_string (LexState *ls, int del, SemInfo *seminfo) {
** or TK_COMMAND_INTERP if an interpolation ${...} was found.
*/
static int read_command_body (LexState *ls, SemInfo *seminfo) {
int interactive = (ls->cmd_mode == 2);
luaZ_resetbuffer(ls->buff);
for (;;) {
switch (ls->current) {
case EOZ:
if (interactive) {
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->cmd_mode = 0;
return TK_INTERACTIVE;
}
lexerror(ls, "unfinished command", TK_EOS);
break; /* to avoid warnings */
case '\n':
case '\r':
if (interactive) {
/* newline terminates interactive command (don't consume it —
leave for inclinenumber on next llex() call) */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->cmd_mode = 0;
return TK_INTERACTIVE;
}
save(ls, '\n');
inclinenumber(ls);
break;
@@ -496,8 +512,7 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
/* store fragment so far */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->in_command = 1;
return TK_COMMAND_INTERP;
return interactive ? TK_INTERACTIVE_INTERP : TK_COMMAND_INTERP;
}
else {
save(ls, '$'); /* not an interpolation, keep literal '$' */
@@ -505,10 +520,15 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
break;
}
case '`': {
if (interactive) {
/* backtick is literal in interactive mode */
save_and_next(ls);
break;
}
next(ls); /* skip closing '`' */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->in_command = 0;
ls->cmd_mode = 0;
return TK_COMMAND;
}
case '\\': { /* escape sequences */
@@ -541,6 +561,7 @@ static int read_command_body (LexState *ls, SemInfo *seminfo) {
static int read_command (LexState *ls, SemInfo *seminfo) {
next(ls); /* skip opening '`' */
ls->cmd_mode = 1;
return read_command_body(ls, seminfo);
}
@@ -636,6 +657,11 @@ static int llex (LexState *ls, SemInfo *seminfo) {
case '`': { /* explicit command `ls -l` with ${} interpolation */
return read_command(ls, seminfo);
}
case '!': { /* interactive command !ls -l */
next(ls); /* skip '!' */
ls->cmd_mode = 2;
return read_command_body(ls, seminfo);
}
case '$': { /* environment variable $NAME */
next(ls);
if (lislalpha(ls->current)) { /* $NAME */