118 lines
4.2 KiB
Markdown
118 lines
4.2 KiB
Markdown
# 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.
|
|
|
|
```lua
|
|
-- 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
|
|
|
|
1. **Brace expansion** (textual, first pass): scan each token for `{a,b,...}` patterns. Expand into multiple tokens. No filesystem access needed.
|
|
|
|
2. **Glob matching** (filesystem, second pass): for each token containing `*`, `?`, or `[`, call `glob(3)` (POSIX) to expand against the filesystem. If no matches, keep the literal token (like bash default).
|
|
|
|
3. **`**` recursive matching**: `glob(3)` doesn't support `**` on all platforms. May need a custom recursive walk, or use `GLOB_ALTDIRFUNC` where available. Could also use `nftw()` + `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 `nullglob` off)
|
|
- **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
|
|
|
|
```lua
|
|
-- 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 |
|