Rewrite #10 with refined interactive command design: bare-word REPL fallback instead of ! prefix, result table assigned to _, same shape as captured commands. Add #11 for Ctrl-C clearing the current line instead of exiting the REPL.
3.9 KiB
Issue #10 — Interactive command execution
Status: open
The two modes
Lush has two distinct ways to run external commands:
-
Captured — backtick syntax, pipes stdout/stderr, returns a table:
local r = `ls -l` r.code -- exit code r.stdout -- captured stdout r.stderr -- captured stderr -
Interactive — bare command (no backticks), inherits the terminal, the process owns stdin/stdout/stderr directly:
> ls -l -- output goes straight to terminal > vim foo.lua -- full tty control, works properly > ssh user@host -- interactive session
This mirrors how traditional shells work: commands run interactively by default, and you explicitly capture output when you want it (in bash: var=$(cmd); in lush: var = `cmd`).
Syntax — bare-word REPL fallback
If a line fails to parse as valid Lua, try to interpret it as an interactive shell command:
> vim foo.lua -- not valid Lua → runs as shell command
> ls -la -- not valid Lua → runs as shell command
> local x = 1 -- valid Lua → runs as Lua
> x = `ls`.stdout -- valid Lua → runs as Lua (captured)
This only applies in the REPL. In scripts, use backtick syntax for captured commands (interactive commands in scripts are a separate question — possibly via ! prefix or exec(), deferred).
Return value and _
Interactive commands can't capture stdout/stderr (the process owns the terminal). However, the exit code is still useful. After an interactive command completes, lush assigns a result table to the global _:
> ls -l
(output appears directly in terminal)
> _.code
0
The table has the same shape as a captured command result, but with empty stdout/stderr:
{code = <exit_code>, stdout = "", stderr = ""}
This keeps the interface consistent — _ always has .code, .stdout, .stderr, whether the command was interactive or captured. Code that only checks .code works with both.
Why _?
_is largely unused in Lua beyond the community convention of throwaway variables in unpacking (local _, b = f())- This convention is safe here: backtick commands return a hashmap table, not a sequence, so
local _, x = \cmd`isn't meaningful anyway (you'd use`cmd`.stdout`) - Bash uses
$?for the last exit code —_serves a similar role
Runtime implementation
Add luaB_interactive to lcmd.c, registered as __interactive global:
fork()without creating any pipes- Child: restore
SIG_DFLforSIGINT/SIGQUIT, thenexecvp()(inherits parent's stdin/stdout/stderr) - Parent: ignore
SIGINT/SIGQUIT(so Ctrl-C goes to child, not lush), thenwaitpid()and return result table - Reuses existing
parse_argv()for command string tokenization
REPL implementation
In lua.c's loadline(), after both addreturn() and multiline() fail:
- Get the raw input line
- Pass it to
__interactive(line) - Assign the result to
_ - Continue the REPL loop (don't report a Lua syntax error)
Open questions
- Should
_be set after captured (backtick) commands too? (So_.codealways reflects the most recent command regardless of mode.) - Should the bare-word fallback support
${}interpolation? (Probably not in the REPL fallback — the line isn't parsed by the lexer at all, it's just a raw string.) - Job control: should Ctrl-Z suspend the child and return to lush? Requires
tcsetpgrp()/ process group work. Defer to a later issue. - How to handle bare commands in scripts (not the REPL)? Defer —
!prefix orexec()builtin are options for later.
Files touched
| File | Description |
|---|---|
lcmd.c |
Add luaB_interactive — fork/exec without pipes, return result table |
lcmd.h |
Declare luaB_interactive |
linit.c |
Register __interactive global in opencommand() |
lua.c |
REPL fallback: on parse failure, try as interactive command, assign result to _ |