4.2 KiB
Issue #13 — Shell globbing
Status: closed Blocked by: #03, #04
Problem
Commands inside backticks have no glob expansion. The argv parser treats *, ?, and {...} as literal characters, so patterns like ls *.lua pass *.lua as a literal string to the command rather than expanding it to matching filenames.
-- currently: passes literal "*.lua" to ls
`ls *.lua`
-- expected: expands to matching files, like a shell would
`ls *.lua` -- becomes ls foo.lua bar.lua ...
Glob patterns to support
| Pattern | Description | Example |
|---|---|---|
* |
Match any characters (not /) |
*.lua → foo.lua bar.lua |
? |
Match single character | ?.c → a.c b.c |
** |
Recursive directory match | **/*.lua → src/a.lua lib/b.lua |
{a,b} |
Brace expansion | *.{o,h} → foo.o bar.h |
[abc] |
Character class | [Mm]akefile → Makefile makefile |
Brace expansion details
Brace expansion happens before glob matching (same as bash). It's purely textual — not a filesystem operation:
echo {o,h} → o h (two words)
echo hello{o,h} → helloo helloh (prefix attached)
echo hello{world,go} → helloworld hellogo (prefix attached)
echo *.{o,h} → (expand braces first, then glob each: *.o *.h)
echo hello{world, x} → hello{world, x} (space inside braces = literal, no expansion)
Rules:
- Braces with commas and no spaces expand:
{a,b,c}→a b c - Prefix/suffix attach to each alternative:
pre{a,b}suf→preasuf prebsuf - Spaces inside braces cancel the expansion (treated literally)
- Nested braces: not needed initially
Implementation
Glob expansion should happen in lcmd.c during argv construction, after tokenizing but before execvp(). Each token that contains glob metacharacters (*, ?, [, {) gets expanded into zero or more filenames.
Approach
-
Brace expansion (textual, first pass): scan each token for
{a,b,...}patterns. Expand into multiple tokens. No filesystem access needed. -
Glob matching (filesystem, second pass): for each token containing
*,?, or[, callglob(3)(POSIX) to expand against the filesystem. If no matches, keep the literal token (like bash default). -
**recursive matching:glob(3)doesn't support**on all platforms. May need a custom recursive walk, or useGLOB_ALTDIRFUNCwhere available. Could also usenftw()+fnmatch().
Where it runs
Expansion happens inside parse_argv() or in a post-processing step after parse_argv() returns. Each original token potentially becomes multiple argv entries.
Quoting suppresses globbing
Quoted strings are already literal in parse_argv():
"*.lua"→ literal*.lua(no expansion)'*.o'→ literal*.o(no expansion)\*→ literal*(backslash escape)
This matches shell behaviour — quoting suppresses glob expansion.
Edge cases
- No matches: keep the literal pattern (bash default behaviour with
nullgloboff) - Dot files:
*should not match files starting with.unless the pattern starts with.(standard shell convention) - Expansion in pipelines: each pipeline stage gets its own glob expansion
- Very large expansions:
**/*in a large tree could produce thousands of entries — may need a reasonable limit
Tests
-- basic wildcard
local r = `echo *.lua`
-- stdout should contain space-separated .lua files (non-empty)
-- no match keeps literal
local r = `echo *.nonexistent_extension_xyz`
assert(r.stdout == "*.nonexistent_extension_xyz\n")
-- quoted glob is literal
local r = `echo "*.lua"`
assert(r.stdout == "*.lua\n")
-- single-quoted glob is literal
local r = `echo '*.lua'`
assert(r.stdout == "*.lua\n")
-- brace expansion
local r = `echo {a,b,c}`
assert(r.stdout == "a b c\n")
-- brace expansion with prefix
local r = `echo hello{world,there}`
assert(r.stdout == "helloworld hellothere\n")
-- brace with spaces = no expansion
local r = `echo {a, b}`
assert(r.stdout == "{a, b}\n")
-- ? single char match
-- [abc] character class
Files to modify
| File | Change |
|---|---|
lcmd.c |
Add brace expansion + glob expansion in argv construction |