# Issue #14 — Prefix-based interactive command execution **Status:** done **Alternative to:** #10, #12 ## Motivation Issue #10 describes bare-word interactive commands via a parser heuristic. The architecture is complex: - An 8-entry exception list to distinguish `ls -la` (shell) from `print("hello")` (Lua) - Raw source buffer position tracking to work around `--` being consumed as a Lua comment - A runtime fallback chain for edge cases like `echo "hello"` parsing as `echo("hello")` - Issue #12 adding further parser rules for path-based commands (`./script`, `/bin/ls`) - Four distinct fallback stages (parser heuristic → normal Lua → runtime retry → post-hoc REPL) This issue proposes a simpler alternative: a `!` prefix to explicitly mark interactive commands. ## Syntax A `!` at statement position introduces an interactive command. Everything after `!` to end-of-line is the command string. ``` > !ls -la > !vim foo.lua > !git commit -m "fix bug" > !ssh user@host > !./script.sh --deploy > !/usr/bin/env python3 > !docker compose up -d ``` ### Why `!`? - `!` is not used in Lua syntax (`not` for logical negation, `~` for bitwise NOT) — zero conflict at statement position - Familiar precedent: vim (`:!cmd`), IPython/Jupyter (`!cmd`), ed/ex (`!cmd`) - Single character, minimal friction ## Comparison with bare-word approach (#10) | | Bare-word (#10) | Prefix (#14) | | --- | --- | --- | | Typing | `ls -la` | `!ls -la` | | Parser complexity | High (exception list, raw source tracking, `--` workaround) | Minimal (`!` token → capture rest of line) | | Runtime fallback | Required (`echo "hello"` edge case) | Not needed | | Path commands | Requires issue #12 | Works naturally (`!./script.sh`) | | `--` flags | Special handling (conflicts with Lua comments) | No conflict (raw capture after `!`) | | Ambiguity | Subtle cases exist | None | | Works in scripts | Yes (inside blocks) | Yes (inside blocks) | The trade-off: one extra character per command, but dramatically simpler implementation with no edge cases. ## Parser implementation When the lexer sees `!` at statement position: 1. Record the current source position (character after `!`) 2. Extract raw text to end-of-line from the source buffer 3. The parser emits bytecode equivalent to `_ = __interactive("raw line text")` 4. Advance the lexer past the consumed line No heuristic, no exception list, no runtime fallback needed. ## Return value and `_` Interactive commands inherit the terminal — stdout/stderr are not captured. After the command completes, the result is assigned to `_`: ``` > !ls -l (output appears directly in terminal) > _.code 0 ``` The table shape matches captured commands, but with blank stdout/stderr: ```lua {code = , stdout = "", stderr = ""} ``` This keeps the interface consistent — code that checks `.code` works with both interactive and captured commands. ## Runtime implementation Shares core logic with backtick commands in `lcmd.c`: - Reuses `parse_argv()` for command string tokenization - `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()` - Build result table `{code=N, stdout="", stderr=""}` and assign to `_` ## Examples ``` -- basic usage > !ls -la > !git status > !vim config.lua > !make -j8 -- path-based commands (no special handling needed) > !./configure --prefix=/usr > !/usr/bin/env python3 -- double-dash flags (no comment conflict) > !git --version > !grep --include="*.lua" -r "pattern" . -- check exit code > !make test > if _.code ~= 0 then print("tests failed") end -- in scripts !cargo build --release if _.code ~= 0 then print("build failed: " .. _.code) os.exit(1) end ``` ## What comes for free - **Interpolation**: the command string passes through the same lexer pipeline as backtick strings, so `${expr}` interpolation works without any additional handling. - **Piping**: the command string goes through `split_pipeline()` (issue #06) before execution, so `!ls -l | grep foo` works — earlier stages inherit the terminal for stderr, the last stage inherits it for stdout too. ## Open questions - Should `_` also be set after captured (backtick) commands? - Could both approaches coexist? `!` for the simple/reliable path, bare-word (#10) added later as sugar on top. ## Files touched | File | Change | |------|--------| | `lcmd.c` | Add `luaB_interactive` (fork/exec without pipes) | | `lcmd.h` | Declare new function | | `linit.c` | Register `__interactive` global in `opencommand()` | | `llex.c` | Recognise `!` at statement position, extract raw line text | | `lparser.c` | Emit `_ = __interactive("raw text")` when `!` detected at statement position |