Add !command syntax that runs commands with inherited terminal (no
capture). The lexer treats ! as a statement-starting token, reading
to end-of-line with the same interpolation/escape/pipe support as
backtick commands. The C function sets _ = {code=N, stdout="", stderr=""}.
140 lines
5.3 KiB
Markdown
140 lines
5.3 KiB
Markdown
# 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 = <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 |
|