Files
lush/issues/10-interactive-command-execution.md
Cormac Shannon 4b49907ce7 Update project issues: resolve #02-#04, add #08-#10
Mark issues #02 (backtick lexing/parsing), #03 (command execution
runtime), and #04 (argv parsing) as resolved. Add new issues for
configuration (#08), programmable prompt (#09), and interactive
command execution (#10).
2026-02-28 18:27:55 +00:00

4.5 KiB

Issue #10 — Interactive command execution

Problem

All backtick commands redirect stdout/stderr to pipes and capture output into a table {code, stdout, stderr}. Interactive programs lose their tty connection:

`bash`    -- launches bash but it can't read/write the terminal
`vim foo` -- screen is blank, no input works

A real shell needs to hand the terminal to a child process and wait for it to finish.

What interactive execution means

The child process inherits all three file descriptors (stdin, stdout, stderr) directly from the parent. The parent waitpid()s until the child exits. The terminal behaves as if lush isn't there — the child owns it.

Syntax

A ! token marks a command as interactive. The command string extends to end-of-line (no closing delimiter needed):

!bash
!vim foo.lua
!ssh user@host

! doesn't conflict with Lua syntax (not is the logical-not keyword). Supports ${} interpolation like backticks:

local f = "foo.lua"
!vim ${f}

Option B: Bare-word REPL fallback (separate issue?)

If a line fails to parse as Lua, interpret it as an interactive command:

> vim foo.lua        -- not valid Lua, runs interactively
> ls -la             -- same
> local x = 1        -- valid Lua, runs as Lua

Only applies in the REPL, not scripts. Could be combined with Option A. Touches lua.c (REPL loop) rather than the lexer/parser, so may warrant its own issue.

Option C: Built-in function

exec("bash")
exec("vim", "foo.lua")

No new syntax. Could serve as the low-level primitive that ! compiles down to.

Return value

Interactive commands return the exit code as a plain integer:

local code = !bash
assert(type(code) == "number")

No stdout/stderr to capture — they go to the terminal. Returning an integer (not a table) clearly distinguishes interactive from captured commands.

When used as a statement (no assignment), the return value is discarded.

Runtime implementation

Add luaB_interactive to lcmd.c, registered as __interactive global:

  • fork() without creating any pipes
  • Child: execvp() directly (inherits parent's stdin/stdout/stderr)
  • Parent: ignore SIGINT/SIGQUIT (so Ctrl-C goes to child, not lush), then waitpid() and return exit code as integer
  • Child: restore SIG_DFL for SIGINT/SIGQUIT before execvp()
  • Reuses existing parse_argv() for command string tokenization

Lexer changes (if using ! prefix)

  • Add TK_ICMD and TK_ICMD_INTERP tokens to enum in llex.h
  • Add in_icmd flag to LexState (mirrors in_command for backticks)
  • read_icmd_body(): like read_command_body() but terminates on newline/EOF instead of closing backtick
  • read_icmd(): skips leading whitespace after !, then calls read_icmd_body()
  • luaX_readicmdcont(): continuation reader after ${} interpolation (mirrors luaX_readcommandcont())
  • Handle ! case in llex() switch: when ! is followed by a non-= character at statement position, read as interactive command

Parser changes (if using ! prefix)

  • Add interactiveexp() in lparser.c — mirrors commandexp() but compiles to __interactive(cmdstring) instead of __command(cmdstring)
  • Add TK_ICMD / TK_ICMD_INTERP cases in simpleexp()
  • Both simple and interpolated forms follow the same pattern as backtick commands

Open questions

  • Which syntax option(s) to pursue? Could start with ! prefix and add others later.
  • Job control: should Ctrl-Z suspend the child and return to lush? Requires tcsetpgrp() / process group work. Probably defer to a later issue.
  • Should bare-word REPL fallback (Option B) be a separate issue?
  • PATH lookup: execvp already searches PATH, so this should just work.
  • Should ! support multi-line commands (e.g. with \ continuation)? Probably not — keep it simple.

Files touched

File Description
lcmd.c Add luaB_interactive — fork/exec without pipes, return exit code
lcmd.h Declare luaB_interactive
linit.c Register __interactive global in opencommand()
llex.h Add TK_ICMD, TK_ICMD_INTERP tokens; in_icmd flag on LexState; declare luaX_readicmdcont()
llex.c Add read_icmd_body(), read_icmd(), luaX_readicmdcont(); handle ! in llex()
lparser.c Add interactiveexp(); handle TK_ICMD/TK_ICMD_INTERP in simpleexp()
lua.c (Only if doing Option B) REPL fallback logic in loadline()