From 75098da240130c602a3483e5b3a90c1f1c725cc3 Mon Sep 17 00:00:00 2001 From: Cormac Shannon <> Date: Mon, 2 Mar 2026 21:16:06 +0000 Subject: [PATCH] Add issue #14: prefix-based interactive commands via ! --- issues/14-prefix-interactive-commands.md | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 issues/14-prefix-interactive-commands.md diff --git a/issues/14-prefix-interactive-commands.md b/issues/14-prefix-interactive-commands.md new file mode 100644 index 00000000..7c79f51e --- /dev/null +++ b/issues/14-prefix-interactive-commands.md @@ -0,0 +1,139 @@ +# Issue #14 — Prefix-based interactive command execution + +**Status:** open +**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 |