Files
lush/issues/22-implicit-interactive-commands.md
Cormac Shannon 08df164692 Add issues #25, #26, #27; update #7 and #22 statuses
- #25: $VAR expansion in commands
- #26: Shell abbreviations and user-extensible builtins
- #27: $(cmd) subcommand syntax
- #22: Updated with implementation details (in progress)
- #7: Standardize status label
2026-03-15 19:33:19 +00:00

75 lines
3.1 KiB
Markdown

# 22 — Implicit interactive commands (drop `!` prefix)
**Status:** in progress
## Goal
In the REPL, treat unrecognized input as shell commands so users can type `git status` or `ls -la` without the `!` prefix.
## Implementation
### REPL fallback chain (`loadline()` in `lua.c`)
The standard Lua REPL tries two compilations. We add a third:
1. Try as expression (`return <line>`)
2. Try as statement (with continuation for incomplete input)
3. **Try as shell command (`!<line>`)**
This is done by adding `addshellcmd()` which prepends `!` to the line and compiles it, triggering the existing lexer/parser path for interactive commands.
### The bare identifier problem
Multi-word input like `git status` fails both Lua paths and correctly falls through to shell. But single-word commands like `ls` compile as `return ls;` — a valid Lua expression that returns `nil` — so they never reach the shell fallback.
Worse: if `addreturn` is bypassed, `ls` as a statement is *incomplete* Lua (the parser expects `ls(...)` or `ls = ...`), so `multiline()` enters continuation mode and the REPL hangs waiting for more input.
### Fix: check `_G` before the expression path
Before trying `return <line>`, check if the line is a bare identifier (single word matching `[a-zA-Z_][a-zA-Z0-9_]*`). If it is, look it up in `_G`:
- **In `_G`** → proceed normally (`return print` shows the function, `return x` shows its value)
- **Not in `_G`** → skip expression and statement paths entirely, go straight to shell
This is a compile-time check — no runtime error interception, no flags, no special subroutines. ~10 lines of C in `loadline()`:
```c
line = lua_tostring(L, 1);
bare = isbareid(line);
if (bare && lua_getglobal(L, line) == LUA_TNIL) {
lua_pop(L, 1);
status = addshellcmd(L); /* straight to shell */
}
else {
if (bare) lua_pop(L, 1);
/* normal expression → statement → shell fallback chain */
}
```
### Summary of behavior
| Input | Path taken | Result |
|-------|-----------|--------|
| `print("hi")` | expression | Lua expression |
| `x = 42` | statement | Lua statement |
| `if true then print("x") end` | statement | Lua statement |
| `print` | expression (in `_G`) | shows function value |
| `x` (after `x=42`) | expression (in `_G`) | shows 42 |
| `git status` | expression fails → statement fails → shell | runs git |
| `ls -la` | expression fails → statement fails → shell | runs ls |
| `echo hello` | expression fails → statement fails → shell | runs echo |
| `ls` | bare id, not in `_G` → shell | runs ls |
| `!pwd` | expression (already valid `!` syntax) | runs pwd |
### Edge cases
- A global explicitly set to `nil` (`x = nil`) would still try the expression path — `return x` compiles and returns nil. This is correct: the user defined it, Lua should handle it.
- Lua keywords (`if`, `for`, `while`) are not valid identifiers in `isbareid` terms — they fail `addreturn`, enter `multiline` for continuation, and behave normally.
- `!cmd` still works — it compiles as a valid expression in the first path.
## Files
| File | Changes |
|------|---------|
| `lua.c` | `isbareid()`, `addshellcmd()`, modified `loadline()` |