Redesign issue #10: bare-word commands now work in both scripts and the REPL via a parser-level heuristic (identifier + non-exception-list token → shell command). Add runtime fallback for string-arg syntax (echo "hello"), double-dash flag handling, and classification examples. Add issue #12 for path-based command execution (./script, /bin/ls, ~/bin/deploy). Add testes/lush/commands-interactive.lua as a design playground covering result table structure, exit codes, commands inside Lua blocks, _ behaviour, runtime fallback, Lua variable shadowing, and interleaved Lua/shell.
This commit is contained in:
361
testes/lush/commands-interactive.lua
Normal file
361
testes/lush/commands-interactive.lua
Normal file
@@ -0,0 +1,361 @@
|
||||
-- testes/lush/commands-interactive.lua
|
||||
-- Tests for interactive command execution (issue #10).
|
||||
-- This file serves as a design playground: it documents how bare-word
|
||||
-- commands should behave alongside Lua in both scripts and the REPL.
|
||||
|
||||
print "testing interactive commands"
|
||||
|
||||
-- ===== RESULT TABLE STRUCTURE =====
|
||||
|
||||
-- basic command, result is a table with code/stdout/stderr
|
||||
do
|
||||
echo hello
|
||||
assert(type(_) == "table")
|
||||
assert(type(_.code) == "number")
|
||||
assert(type(_.stdout) == "string")
|
||||
assert(type(_.stderr) == "string")
|
||||
end
|
||||
|
||||
-- ===== EXIT CODES =====
|
||||
|
||||
-- successful command returns exit code 0
|
||||
do
|
||||
sh -c "exit 0"
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- failed command returns non-zero exit code
|
||||
do
|
||||
sh -c "exit 1"
|
||||
assert(_.code == 1)
|
||||
end
|
||||
|
||||
-- specific exit codes are preserved
|
||||
do
|
||||
sh -c "exit 42"
|
||||
assert(_.code == 42)
|
||||
end
|
||||
|
||||
-- command not found returns 127
|
||||
do
|
||||
nonexistent_command_xyz_999
|
||||
assert(_.code == 127)
|
||||
end
|
||||
|
||||
-- ===== INTERACTIVE MODE: NO STDOUT/STDERR CAPTURE =====
|
||||
-- interactive commands inherit the terminal; stdout/stderr go directly
|
||||
-- to the user's screen, so _.stdout and _.stderr are always empty.
|
||||
|
||||
do
|
||||
echo hello
|
||||
assert(_.stdout == "")
|
||||
end
|
||||
|
||||
do
|
||||
sh -c "echo err >&2"
|
||||
assert(_.stderr == "")
|
||||
assert(_.stdout == "")
|
||||
end
|
||||
|
||||
do
|
||||
sh -c "echo out; echo err >&2"
|
||||
assert(_.stdout == "")
|
||||
assert(_.stderr == "")
|
||||
end
|
||||
|
||||
do
|
||||
echo hello world
|
||||
assert(_.stdout == "")
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- ===== PARSER HEURISTIC: IDENTIFIER + NON-EXCEPTION TOKEN =====
|
||||
-- the parser detects shell commands when an identifier at statement
|
||||
-- position is followed by a token NOT in the exception list:
|
||||
-- ( string { . : [ = ,
|
||||
|
||||
-- bare identifier, no arguments (next token is keyword/identifier/EOF)
|
||||
do
|
||||
ls
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- identifier + dash flag
|
||||
do
|
||||
ls -la /
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- identifier + slash (path argument)
|
||||
do
|
||||
ls /tmp
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- identifier + identifier (subcommand pattern)
|
||||
do
|
||||
git --version
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- ===== COMMANDS INSIDE LUA BLOCKS =====
|
||||
-- bare commands work anywhere a Lua statement can appear.
|
||||
|
||||
-- inside do/end
|
||||
do
|
||||
echo inside-do
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- inside if/end
|
||||
do
|
||||
if true then
|
||||
echo inside-if
|
||||
assert(_.code == 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- inside for/end
|
||||
do
|
||||
for i = 1, 3 do
|
||||
echo loop
|
||||
assert(_.code == 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- inside while/end
|
||||
do
|
||||
local n = 0
|
||||
while n < 2 do
|
||||
echo while-loop
|
||||
assert(_.code == 0)
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- inside function body
|
||||
do
|
||||
local function run_cmd()
|
||||
ls /
|
||||
return _.code
|
||||
end
|
||||
assert(run_cmd() == 0)
|
||||
end
|
||||
|
||||
-- nested blocks
|
||||
do
|
||||
if true then
|
||||
for i = 1, 2 do
|
||||
echo nested
|
||||
assert(_.code == 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== _ BEHAVIOR =====
|
||||
|
||||
-- _ is overwritten by subsequent commands
|
||||
do
|
||||
sh -c "exit 0"
|
||||
assert(_.code == 0)
|
||||
sh -c "exit 5"
|
||||
assert(_.code == 5)
|
||||
end
|
||||
|
||||
-- _ persists across block boundaries (it's a global)
|
||||
do
|
||||
sh -c "exit 3"
|
||||
end
|
||||
assert(_.code == 3)
|
||||
|
||||
-- ===== RUNTIME FALLBACK: STRING-ARG FUNCTION CALL SYNTAX =====
|
||||
-- echo "hello" parses as Lua echo("hello") because string literal
|
||||
-- is in the exception list. at runtime, echo is nil → "attempt to
|
||||
-- call a nil value" → check PATH → found → run as shell command.
|
||||
|
||||
do
|
||||
echo "hello"
|
||||
assert(_.code == 0)
|
||||
assert(_.stdout == "")
|
||||
end
|
||||
|
||||
do
|
||||
printf "hello\n"
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
-- undefined name NOT in PATH → original Lua error preserved
|
||||
do
|
||||
local ok, err = pcall(function()
|
||||
pirnt "hello"
|
||||
end)
|
||||
assert(not ok)
|
||||
assert(string.find(err, "pirnt"))
|
||||
end
|
||||
|
||||
-- ===== LUA VARIABLE SHADOWS SHELL COMMAND =====
|
||||
-- if a Lua variable with the same name as a shell command is defined,
|
||||
-- the Lua variable wins. the shell fallback only triggers on nil.
|
||||
|
||||
-- string-arg sugar: echo "hello" parses as echo("hello").
|
||||
-- echo is a local function, so Lua calls it — NOT /bin/echo.
|
||||
do
|
||||
local func_called = false
|
||||
local echo = function(x) func_called = true end
|
||||
echo "hello"
|
||||
assert(func_called == true)
|
||||
end
|
||||
|
||||
-- table-arg sugar: same principle with { } syntax
|
||||
do
|
||||
local received = nil
|
||||
local grep = function(t) received = t end
|
||||
grep {"pattern", "file.txt"}
|
||||
assert(received[1] == "pattern")
|
||||
end
|
||||
|
||||
-- paren call: unambiguously Lua, local wins
|
||||
do
|
||||
local func_called = false
|
||||
local ls = function(...) func_called = true end
|
||||
ls("/tmp")
|
||||
assert(func_called == true)
|
||||
end
|
||||
|
||||
-- global function shadows command name
|
||||
do
|
||||
local func_called = false
|
||||
function echo(x) func_called = true end
|
||||
echo "test"
|
||||
assert(func_called == true)
|
||||
echo = nil -- clean up global
|
||||
end
|
||||
|
||||
-- ===== LUA SYNTAX PRESERVED =====
|
||||
-- all exception-list tokens correctly route to Lua parsing.
|
||||
|
||||
-- multi-line function call: string arg on next line
|
||||
do
|
||||
local function my_func(arg)
|
||||
return arg
|
||||
end
|
||||
|
||||
local r = my_func
|
||||
"hello world"
|
||||
assert(r == "hello world")
|
||||
end
|
||||
|
||||
-- multi-line function call: paren arg on next line
|
||||
do
|
||||
local function add(a, b) return a + b end
|
||||
|
||||
local r = add
|
||||
(1, 2)
|
||||
assert(r == 3)
|
||||
end
|
||||
|
||||
-- multi-line function call: table arg on next line
|
||||
do
|
||||
local function first(t) return t[1] end
|
||||
|
||||
local r = first
|
||||
{42}
|
||||
assert(r == 42)
|
||||
end
|
||||
|
||||
-- assignment
|
||||
do
|
||||
local x = 5
|
||||
assert(x == 5)
|
||||
end
|
||||
|
||||
-- multi-assignment
|
||||
do
|
||||
local a, b = 1, 2
|
||||
assert(a == 1 and b == 2)
|
||||
end
|
||||
|
||||
-- field access
|
||||
do
|
||||
local t = {field = 10}
|
||||
assert(t.field == 10)
|
||||
t.field = 20
|
||||
assert(t.field == 20)
|
||||
end
|
||||
|
||||
-- method calls
|
||||
do
|
||||
local s = "hello"
|
||||
assert(s:upper() == "HELLO")
|
||||
end
|
||||
|
||||
-- indexing
|
||||
do
|
||||
local t = {10, 20, 30}
|
||||
assert(t[2] == 20)
|
||||
t[2] = 99
|
||||
assert(t[2] == 99)
|
||||
end
|
||||
|
||||
-- table-arg function call
|
||||
do
|
||||
local function f(t) return t[1] end
|
||||
assert(f {42} == 42)
|
||||
end
|
||||
|
||||
-- keyword-led statements
|
||||
do
|
||||
local x = 1
|
||||
if x == 1 then x = 2 end
|
||||
assert(x == 2)
|
||||
for i = 1, 1 do x = 3 end
|
||||
assert(x == 3)
|
||||
while x > 3 do x = x - 1 end
|
||||
assert(x == 3)
|
||||
repeat x = x - 1 until x == 0
|
||||
assert(x == 0)
|
||||
end
|
||||
|
||||
-- ===== INTERLEAVED LUA AND SHELL =====
|
||||
|
||||
do
|
||||
local x = 10
|
||||
ls /
|
||||
assert(_.code == 0)
|
||||
local y = x + 20
|
||||
assert(y == 30)
|
||||
echo hello
|
||||
assert(_.code == 0)
|
||||
local z = y * 2
|
||||
assert(z == 60)
|
||||
end
|
||||
|
||||
-- ===== EDGE CASES =====
|
||||
|
||||
-- double-dash flags (--) look like Lua comments to the lexer.
|
||||
-- the parser must capture raw source text BEFORE the lexer consumes
|
||||
-- the comment, so the full argument string is preserved.
|
||||
do
|
||||
git --version
|
||||
assert(_.code == 0)
|
||||
ls --color=auto /tmp
|
||||
assert(_.code == 0) -- may fail if ls doesn't support --color
|
||||
end
|
||||
|
||||
-- commands where first arg is another known command name
|
||||
do
|
||||
env ls
|
||||
-- env runs ls; both are valid commands, this is identifier + identifier
|
||||
assert(type(_.code) == "number")
|
||||
end
|
||||
|
||||
-- semicolons: Lua uses ; as optional statement separator.
|
||||
-- with the heuristic, ls followed by ; is ambiguous.
|
||||
-- for now, use separate lines instead:
|
||||
do
|
||||
ls /tmp
|
||||
echo done
|
||||
assert(_.code == 0)
|
||||
end
|
||||
|
||||
print "OK"
|
||||
Reference in New Issue
Block a user