Files
lush/issues/13-shell-globbing.md
2026-03-02 00:37:13 +00:00

4.2 KiB

Issue #13 — Shell globbing

Status: open 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 /) *.luafoo.lua bar.lua
? Match single character ?.ca.c b.c
** Recursive directory match **/*.luasrc/a.lua lib/b.lua
{a,b} Brace expansion *.{o,h}foo.o bar.h
[abc] Character class [Mm]akefileMakefile 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}sufpreasuf 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

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