Implement shell globbing, tilde expansion, and brace expansion (issues #13, #18)

Unquoted tokens are now expanded through a tilde → brace → glob pipeline
in parse_argv. Quoted tokens (single, double, or backslash) suppress all
expansion. Uses glob(3) with GLOB_NOCHECK for wildcard matching and manual
implementations for tilde (~→$HOME) and brace ({a,b,c}) expansion.
This commit is contained in:
Cormac Shannon
2026-03-10 21:31:55 +00:00
parent c96fae90c0
commit a43fd10e64
2 changed files with 450 additions and 5 deletions

135
testes/lush/globbing.lua Normal file
View File

@@ -0,0 +1,135 @@
-- testes/lush/globbing.lua
-- Tests for shell globbing (#13) and tilde expansion (#18).
print "testing globbing and tilde expansion"
-- === wildcard expansion ===
-- *.lua should expand to matching files (we know lush test files exist)
do
local r = `echo *.c`
local out = r.stdout:gsub("\n$", "")
-- should NOT be the literal "*.c" since .c files exist in the project
assert(out ~= "*.c", "expected *.c to expand, got literal: " .. out)
assert(out:find("lcmd.c"), "expected lcmd.c in expansion, got: " .. out)
end
-- no match: keeps literal pattern (GLOB_NOCHECK)
do
local r = `echo *.nonexistent_xyz_9876`
local out = r.stdout:gsub("\n$", "")
assert(out == "*.nonexistent_xyz_9876",
"expected literal, got: " .. out)
end
-- ? single-char wildcard
do
-- create temp files to test ? pattern
os.execute("mkdir -p /tmp/_lush_glob_test")
os.execute("touch /tmp/_lush_glob_test/a1 /tmp/_lush_glob_test/b2")
local r = `echo /tmp/_lush_glob_test/?1`
local out = r.stdout:gsub("\n$", "")
assert(out == "/tmp/_lush_glob_test/a1",
"expected ?1 match, got: " .. out)
os.execute("rm -rf /tmp/_lush_glob_test")
end
-- === quoting suppresses expansion ===
-- double-quoted glob stays literal
do
local r = `echo "*.c"`
local out = r.stdout:gsub("\n$", "")
assert(out == "*.c", "double-quoted glob expanded: " .. out)
end
-- single-quoted glob stays literal
do
local r = `echo '*.c'`
local out = r.stdout:gsub("\n$", "")
assert(out == "*.c", "single-quoted glob expanded: " .. out)
end
-- backslash-escaped glob stays literal
do
local r = `echo \*.c`
local out = r.stdout:gsub("\n$", "")
assert(out == "*.c", "backslash-escaped glob expanded: " .. out)
end
-- === brace expansion ===
do
local r = `echo {a,b,c}`
local out = r.stdout:gsub("\n$", "")
assert(out == "a b c", "brace expansion failed, got: " .. out)
end
-- brace with prefix
do
local r = `echo hello{world,there}`
local out = r.stdout:gsub("\n$", "")
assert(out == "helloworld hellothere",
"brace prefix expansion failed, got: " .. out)
end
-- space inside braces: no expansion (treated as separate tokens)
do
local r = `echo {a, b}`
local out = r.stdout:gsub("\n$", "")
-- with space after comma, the shell splits into tokens "{a," and "b}"
assert(out == "{a, b}", "space-in-braces should not expand, got: " .. out)
end
-- === tilde expansion ===
-- ~ expands to $HOME
do
local home = os.getenv("HOME")
local r = `echo ~`
local out = r.stdout:gsub("\n$", "")
assert(out == home, "~ did not expand to HOME, got: " .. out)
end
-- ~/path expands to $HOME/path
do
local home = os.getenv("HOME")
local r = `echo ~/foo`
local out = r.stdout:gsub("\n$", "")
assert(out == home .. "/foo",
"~/foo did not expand, got: " .. out)
end
-- quoted ~ stays literal
do
local r = `echo "~"`
local out = r.stdout:gsub("\n$", "")
assert(out == "~", "quoted ~ expanded: " .. out)
end
-- cd ~ should work (builtin receives expanded path)
do
local before = `pwd`.stdout:gsub("\n$", "")
local home = os.getenv("HOME")
local r = `cd ~`
assert(r.code == 0, "cd ~ failed: " .. r.stderr)
local after = `pwd`.stdout:gsub("\n$", "")
assert(after == home, "cd ~ did not go home, got: " .. after)
`cd ${before}`
end
-- === expansion in pipelines ===
do
local r = `echo *.c | head -1`
local out = r.stdout:gsub("\n$", "")
-- pipeline should have expanded *.c before piping
assert(out ~= "*.c", "glob not expanded in pipeline, got: " .. out)
end
print "OK"