Rewrite #10 with refined interactive command design: bare-word REPL fallback instead of ! prefix, result table assigned to _, same shape as captured commands. Add #11 for Ctrl-C clearing the current line instead of exiting the REPL.
97 lines
3.9 KiB
Markdown
97 lines
3.9 KiB
Markdown
# Issue #10 — Interactive command execution
|
|
|
|
**Status:** open
|
|
|
|
## The two modes
|
|
|
|
Lush has two distinct ways to run external commands:
|
|
|
|
1. **Captured** — backtick syntax, pipes stdout/stderr, returns a table:
|
|
```lua
|
|
local r = `ls -l`
|
|
r.code -- exit code
|
|
r.stdout -- captured stdout
|
|
r.stderr -- captured stderr
|
|
```
|
|
|
|
2. **Interactive** — bare command (no backticks), inherits the terminal, the process owns stdin/stdout/stderr directly:
|
|
```
|
|
> ls -l -- output goes straight to terminal
|
|
> vim foo.lua -- full tty control, works properly
|
|
> ssh user@host -- interactive session
|
|
```
|
|
|
|
This mirrors how traditional shells work: commands run interactively by default, and you explicitly capture output when you want it (in bash: `var=$(cmd)`; in lush: `` var = `cmd` ``).
|
|
|
|
## Syntax — bare-word REPL fallback
|
|
|
|
If a line fails to parse as valid Lua, try to interpret it as an interactive shell command:
|
|
|
|
```
|
|
> vim foo.lua -- not valid Lua → runs as shell command
|
|
> ls -la -- not valid Lua → runs as shell command
|
|
> local x = 1 -- valid Lua → runs as Lua
|
|
> x = `ls`.stdout -- valid Lua → runs as Lua (captured)
|
|
```
|
|
|
|
This only applies in the REPL. In scripts, use backtick syntax for captured commands (interactive commands in scripts are a separate question — possibly via `!` prefix or `exec()`, deferred).
|
|
|
|
## Return value and `_`
|
|
|
|
Interactive commands can't capture stdout/stderr (the process owns the terminal). However, the exit code is still useful. After an interactive command completes, lush assigns a result table to the global `_`:
|
|
|
|
```
|
|
> ls -l
|
|
(output appears directly in terminal)
|
|
> _.code
|
|
0
|
|
```
|
|
|
|
The table has the same shape as a captured command result, but with empty stdout/stderr:
|
|
|
|
```lua
|
|
{code = <exit_code>, stdout = "", stderr = ""}
|
|
```
|
|
|
|
This keeps the interface consistent — `_` always has `.code`, `.stdout`, `.stderr`, whether the command was interactive or captured. Code that only checks `.code` works with both.
|
|
|
|
### Why `_`?
|
|
|
|
- `_` is largely unused in Lua beyond the community convention of throwaway variables in unpacking (`local _, b = f()`)
|
|
- This convention is safe here: backtick commands return a hashmap table, not a sequence, so `local _, x = \`cmd\`` isn't meaningful anyway (you'd use `\`cmd\`.stdout`)
|
|
- Bash uses `$?` for the last exit code — `_` serves a similar role
|
|
|
|
## Runtime implementation
|
|
|
|
Add `luaB_interactive` to `lcmd.c`, registered as `__interactive` global:
|
|
|
|
- `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()` and return result table
|
|
- Reuses existing `parse_argv()` for command string tokenization
|
|
|
|
## REPL implementation
|
|
|
|
In `lua.c`'s `loadline()`, after both `addreturn()` and `multiline()` fail:
|
|
|
|
1. Get the raw input line
|
|
2. Pass it to `__interactive(line)`
|
|
3. Assign the result to `_`
|
|
4. Continue the REPL loop (don't report a Lua syntax error)
|
|
|
|
## Open questions
|
|
|
|
- Should `_` be set after captured (backtick) commands too? (So `_.code` always reflects the most recent command regardless of mode.)
|
|
- Should the bare-word fallback support `${}` interpolation? (Probably not in the REPL fallback — the line isn't parsed by the lexer at all, it's just a raw string.)
|
|
- Job control: should Ctrl-Z suspend the child and return to lush? Requires `tcsetpgrp()` / process group work. Defer to a later issue.
|
|
- How to handle bare commands in scripts (not the REPL)? Defer — `!` prefix or `exec()` builtin are options for later.
|
|
|
|
## Files touched
|
|
|
|
| File | Description |
|
|
|------|-------------|
|
|
| `lcmd.c` | Add `luaB_interactive` — fork/exec without pipes, return result table |
|
|
| `lcmd.h` | Declare `luaB_interactive` |
|
|
| `linit.c` | Register `__interactive` global in `opencommand()` |
|
|
| `lua.c` | REPL fallback: on parse failure, try as interactive command, assign result to `_` |
|