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.
This commit is contained in:
@@ -1,114 +1,96 @@
|
||||
# Issue #10 — Interactive command execution
|
||||
|
||||
## Problem
|
||||
**Status:** open
|
||||
|
||||
All backtick commands redirect stdout/stderr to pipes and capture output into a table `{code, stdout, stderr}`. Interactive programs lose their tty connection:
|
||||
## 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
|
||||
`bash` -- launches bash but it can't read/write the terminal
|
||||
`vim foo` -- screen is blank, no input works
|
||||
{code = <exit_code>, stdout = "", stderr = ""}
|
||||
```
|
||||
|
||||
A real shell needs to hand the terminal to a child process and wait for it to finish.
|
||||
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.
|
||||
|
||||
## What interactive execution means
|
||||
### Why `_`?
|
||||
|
||||
The child process inherits all three file descriptors (stdin, stdout, stderr) directly from the parent. The parent `waitpid()`s until the child exits. The terminal behaves as if lush isn't there — the child owns it.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Option A: `!` prefix (recommended starting point)
|
||||
|
||||
A `!` token marks a command as interactive. The command string extends to end-of-line (no closing delimiter needed):
|
||||
|
||||
```lua
|
||||
!bash
|
||||
!vim foo.lua
|
||||
!ssh user@host
|
||||
```
|
||||
|
||||
`!` doesn't conflict with Lua syntax (`not` is the logical-not keyword). Supports `${}` interpolation like backticks:
|
||||
|
||||
```lua
|
||||
local f = "foo.lua"
|
||||
!vim ${f}
|
||||
```
|
||||
|
||||
### Option B: Bare-word REPL fallback (separate issue?)
|
||||
|
||||
If a line fails to parse as Lua, interpret it as an interactive command:
|
||||
|
||||
```
|
||||
> vim foo.lua -- not valid Lua, runs interactively
|
||||
> ls -la -- same
|
||||
> local x = 1 -- valid Lua, runs as Lua
|
||||
```
|
||||
|
||||
Only applies in the REPL, not scripts. Could be combined with Option A. Touches `lua.c` (REPL loop) rather than the lexer/parser, so may warrant its own issue.
|
||||
|
||||
### Option C: Built-in function
|
||||
|
||||
```lua
|
||||
exec("bash")
|
||||
exec("vim", "foo.lua")
|
||||
```
|
||||
|
||||
No new syntax. Could serve as the low-level primitive that `!` compiles down to.
|
||||
|
||||
## Return value
|
||||
|
||||
Interactive commands return the exit code as a plain integer:
|
||||
|
||||
```lua
|
||||
local code = !bash
|
||||
assert(type(code) == "number")
|
||||
```
|
||||
|
||||
No stdout/stderr to capture — they go to the terminal. Returning an integer (not a table) clearly distinguishes interactive from captured commands.
|
||||
|
||||
When used as a statement (no assignment), the return value is discarded.
|
||||
- `_` 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: `execvp()` directly (inherits parent's stdin/stdout/stderr)
|
||||
- Parent: ignore `SIGINT`/`SIGQUIT` (so Ctrl-C goes to child, not lush), then `waitpid()` and return exit code as integer
|
||||
- Child: restore `SIG_DFL` for `SIGINT`/`SIGQUIT` before `execvp()`
|
||||
- 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
|
||||
|
||||
## Lexer changes (if using `!` prefix)
|
||||
## REPL implementation
|
||||
|
||||
- Add `TK_ICMD` and `TK_ICMD_INTERP` tokens to enum in `llex.h`
|
||||
- Add `in_icmd` flag to `LexState` (mirrors `in_command` for backticks)
|
||||
- `read_icmd_body()`: like `read_command_body()` but terminates on newline/EOF instead of closing backtick
|
||||
- `read_icmd()`: skips leading whitespace after `!`, then calls `read_icmd_body()`
|
||||
- `luaX_readicmdcont()`: continuation reader after `${}` interpolation (mirrors `luaX_readcommandcont()`)
|
||||
- Handle `!` case in `llex()` switch: when `!` is followed by a non-`=` character at statement position, read as interactive command
|
||||
In `lua.c`'s `loadline()`, after both `addreturn()` and `multiline()` fail:
|
||||
|
||||
## Parser changes (if using `!` prefix)
|
||||
|
||||
- Add `interactiveexp()` in `lparser.c` — mirrors `commandexp()` but compiles to `__interactive(cmdstring)` instead of `__command(cmdstring)`
|
||||
- Add `TK_ICMD` / `TK_ICMD_INTERP` cases in `simpleexp()`
|
||||
- Both simple and interpolated forms follow the same pattern as backtick commands
|
||||
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
|
||||
|
||||
- Which syntax option(s) to pursue? Could start with `!` prefix and add others later.
|
||||
- Job control: should `Ctrl-Z` suspend the child and return to lush? Requires `tcsetpgrp()` / process group work. Probably defer to a later issue.
|
||||
- Should bare-word REPL fallback (Option B) be a separate issue?
|
||||
- `PATH` lookup: `execvp` already searches `PATH`, so this should just work.
|
||||
- Should `!` support multi-line commands (e.g. with `\` continuation)? Probably not — keep it simple.
|
||||
- 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 exit code |
|
||||
| `lcmd.c` | Add `luaB_interactive` — fork/exec without pipes, return result table |
|
||||
| `lcmd.h` | Declare `luaB_interactive` |
|
||||
| `linit.c` | Register `__interactive` global in `opencommand()` |
|
||||
| `llex.h` | Add `TK_ICMD`, `TK_ICMD_INTERP` tokens; `in_icmd` flag on `LexState`; declare `luaX_readicmdcont()` |
|
||||
| `llex.c` | Add `read_icmd_body()`, `read_icmd()`, `luaX_readicmdcont()`; handle `!` in `llex()` |
|
||||
| `lparser.c` | Add `interactiveexp()`; handle `TK_ICMD`/`TK_ICMD_INTERP` in `simpleexp()` |
|
||||
| `lua.c` | *(Only if doing Option B)* REPL fallback logic in `loadline()` |
|
||||
| `lua.c` | REPL fallback: on parse failure, try as interactive command, assign result to `_` |
|
||||
|
||||
49
issues/11-ctrl-c-repl-behaviour.md
Normal file
49
issues/11-ctrl-c-repl-behaviour.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Issue #11 — Ctrl-C REPL behaviour
|
||||
|
||||
**Status:** open
|
||||
|
||||
## Problem
|
||||
|
||||
The Lua REPL immediately exits on Ctrl-C. This is annoying — every other shell and REPL (bash, zsh, python, node) treats Ctrl-C as "cancel what I'm typing", not "quit".
|
||||
|
||||
## Desired behaviour
|
||||
|
||||
**Dirty line** (cursor on a line with text already typed):
|
||||
- Clear the current input line
|
||||
- Print a fresh prompt on a new line
|
||||
|
||||
```
|
||||
> local x = some_long_exp^C
|
||||
>
|
||||
```
|
||||
|
||||
**Empty line** (no text typed yet):
|
||||
- Print `^C` and a fresh prompt (consistent with bash/zsh)
|
||||
- Optionally: two consecutive Ctrl-C on empty lines could exit (like Python's REPL), but this may be unnecessary if `Ctrl-D` / `exit` works
|
||||
|
||||
## Current behaviour
|
||||
|
||||
The Lua REPL sets a debug hook via `laction()` on `SIGINT` that calls `luaL_error(L, "interrupted!")`. This aborts whatever Lua code is running and also kills the REPL input loop.
|
||||
|
||||
## Implementation approach
|
||||
|
||||
The fix is in `lua.c`'s REPL loop. The signal handler needs to behave differently depending on context:
|
||||
|
||||
**During input** (waiting for user to type a line):
|
||||
- If using readline/libedit: readline already handles `SIGINT` — it clears the line and returns. We just need to not treat this as a fatal error.
|
||||
- If using the `fgets` fallback: set a flag on `SIGINT`, check it after `fgets` returns, clear the buffer and re-prompt.
|
||||
|
||||
**During execution** (running Lua code):
|
||||
- Keep the existing behaviour: interrupt the running code with an error. This is correct — `SIGINT` during a long-running Lua computation should stop it.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should readline's `rl_catch_signals` / signal handling be configured explicitly?
|
||||
- Does libedit (macOS default) handle `SIGINT` the same way as GNU readline?
|
||||
- Should Ctrl-C during a multi-line continuation (prompt `>>`) discard the entire multi-line input and return to the primary prompt?
|
||||
|
||||
## Files touched
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `lua.c` | Modify signal handling in `doREPL()` / `pushline()` to distinguish input vs execution context |
|
||||
Reference in New Issue
Block a user