Add issue #14: prefix-based interactive commands via !
This commit is contained in:
139
issues/14-prefix-interactive-commands.md
Normal file
139
issues/14-prefix-interactive-commands.md
Normal file
@@ -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 = <exit_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 |
|
||||||
Reference in New Issue
Block a user