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

3.1 KiB

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():

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()