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