Files
lush/issues/10-interactive-command-execution.md
Cormac Shannon 47abb04921 Rewrite issue #10, add issue #11 (Ctrl-C REPL behaviour)
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.
2026-02-28 23:20:55 +00:00

3.9 KiB

Issue #10 — Interactive command execution

Status: open

The two modes

Lush has two distinct ways to run external commands:

  1. 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
    
  2. 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_DFL for SIGINT/SIGQUIT, then execvp() (inherits parent's stdin/stdout/stderr)
  • Parent: ignore SIGINT/SIGQUIT (so Ctrl-C goes to child, not lush), then waitpid() 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:

  1. Get the raw input line
  2. Pass it to __interactive(line)
  3. Assign the result to _
  4. Continue the REPL loop (don't report a Lua syntax error)

Open questions

  • Should _ be set after captured (backtick) commands too? (So _.code always 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 or exec() 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 _