Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

btv.glob

btv.glob.match(pattern, path, opts)

btv.glob.match(pattern, path, opts) -> boolean: does path match the glob pattern? The one-shot form — it goes through the same compiled-regex cache as btv.glob.compile, so calling it in a loop over many paths does not recompile. Raises on an invalid pattern.

The glob syntax, shared by every function in this namespace:

*        any run of characters, NOT crossing a `/` (see `literal_separator`)
**       any run of characters, crossing `/`  ( `**/x`, `a/**`, `a/**/b` )
         — and it spans ZERO components too, so `**/x` matches a bare `x`
?        exactly one character — also `/`-stopped, per `literal_separator`
[abc]    one of the listed characters       [a-z] a range
[!abc]   none of them                       `[^abc]` means the same thing
{a,b}    either alternative                 `{a,b/**}` nests
\x       a literal `x` (unix style only — in windows style `\` separates)

Two defaults worth knowing:

  • * stops at /. btv.glob.match("*.lua", "a/b/c.lua") is false; say "**/*.lua", or pass basename = true to match the path’s last component. This is shell / gitignore / LSP semantics. Pass literal_separator = false for vim’s autocmd rule, where * does cross /.
  • Path style defaults to the host’s, so pass it when you know the path’s. A pattern and its candidates are unix-style (/ separates, \ escapes) on a Unix build and windows-style (\ separates too) on a Windows build, unless you pass style = "unix" / style = "windows". Pass it whenever the path’s origin is known: a daemon or remote session can hand a Unix-hosted editor Windows paths, so the convention belongs to the path, not the build.

An invalid pattern raises. The one exception is an unclosed class (foo[bar), which is taken as literal text — a filename may genuinely contain a bracket.

path is matched as bytes, not decoded text: a name that is not valid UTF-8 (a latin-1 filename on disk, a name that arrived over the encoding seam) matches by its real bytes rather than raising or being lossily rewritten. The pattern must be valid UTF-8 — glob syntax is ASCII, so a pattern that isn’t is a mistake worth hearing about.

opts (all optional):

style             = "unix" | "windows"  -- separator convention (default: the host's)
ignorecase        = false               -- ASCII-case-insensitive match
literal_separator = true                -- `*`/`?` stop at a separator
basename          = false               -- a separator-less pattern matches the
                                           path's last component
empty_alternates  = false               -- allow the empty branch in `{a,}`
btv.glob.match("*.lua", "init.lua")            --> true
btv.glob.match("*.lua", "conf/init.lua")       --> false  ( `*` stops at `/` )
btv.glob.match("**/*.lua", "conf/init.lua")    --> true
btv.glob.match("*.lua", "conf/init.lua", { basename = true })  --> true
btv.glob.match("src/**/*.{rs,toml}", "src/a/b/Cargo.toml")     --> true

Defined in glob.lua.

btv.glob.compile(pattern, opts)

btv.glob.compile(pattern, opts) -> glob: compile pattern into a reusable glob object. Equivalent to btv.glob.match for a single test, but worth holding onto when a pattern is stored and matched later (a watcher, a filter kept across events) — it makes the compile explicit and its cost obvious. opts is as documented on btv.glob.match. Raises on an invalid pattern.

The returned object has:

g:test(path)  -> boolean: does `path` match
g:pattern()   -> the glob as written
g:regex()     -> the regex source the glob translated to

g:regex() is for debugging: a glob that mismatches is far easier to reason about against the regex it actually became. It is the regex this glob runs, not a standalone equivalent — under basename = true the path is reduced to its last component before the regex sees it, and that reduction is not in the regex. (Which is why btv.glob.to_regex, whose job is to hand the translation to another engine, refuses a basename glob rather than answering with a regex that means something else.)

local ignore = btv.glob.compile("**/target/**")
if ignore:test(path) then return end

Defined in glob.lua.

btv.glob.set(patterns, opts)

btv.glob.set(patterns, opts) -> globset: compile a list of globs into one matcher. All the patterns share a single regex set, so testing a path against 50 globs is one pass rather than 50 — the shape an ignore list or a set of file-watcher patterns wants. opts is as documented on btv.glob.match and applies to every pattern. An invalid pattern fails the whole set (naming itself), rather than being quietly dropped.

The returned object has:

s:test(path)     -> boolean: does ANY pattern match
s:matches(path)  -> list of the 1-based indices of the patterns that matched
s:patterns()     -> the pattern list as written, in set order
local ignore = btv.glob.set({ "**/.git/**", "**/target/**", "*.tmp" })
local kept = btv.tbl.filter(function(p) return not ignore:test(p) end, paths)

Defined in glob.lua.

btv.glob.any(patterns, path, opts)

btv.glob.any(patterns, path, opts) -> boolean: does any glob in patterns match path? The one-shot form of btv.glob.set — cached the same way, so a repeated call with the same list reuses the compiled set. patterns may also be a single pattern string, so a caller taking “a glob or a list of globs” from user config need not branch.

btv.glob.any({ "*.rs", "*.toml" }, "Cargo.toml")  --> true

Defined in glob.lua.

btv.glob.filter(patterns, paths, opts)

btv.glob.filter(patterns, paths, opts) -> list: the entries of paths matching any glob in patterns, in their original order. patterns may be a single pattern string or a list, as in btv.glob.any. One compiled set for the whole sweep.

btv.async(function()
  local rs = btv.glob.filter({ "**/*.rs" }, btv.await(btv.fs.walk(".")))
end)

Defined in glob.lua.

btv.glob.to_regex(pattern, opts)

btv.glob.to_regex(pattern, opts) -> string: the regex source pattern translates to, without compiling or caching it. For introspection, and for handing the translation to another engine (it is a byte-oriented Rust regex pattern, the dialect btv.regex speaks). opts is as documented on btv.glob.match.

A basename = true glob raises here instead of answering. basename reduces the candidate to its last component before the regex runs; that is a step on the path, not something the pattern can express (with literal_separator = false a * crosses /, so no (?:.*/)? prefix is faithful). Since this function exists to hand the regex to an engine that will not perform the reduction, returning one anyway would be silently wrong — reduce the path yourself, or drop the option.

btv.glob.to_regex("*.lua")  --> "(?-u)^[^/]*\\.lua$"

Defined in glob.lua.

btv.glob.is_glob(s)

btv.glob.is_glob(s) -> boolean: does s carry a glob metacharacter (* ? [ {) — i.e. is it a pattern at all, rather than a plain path? The canonical predicate for code that branches on “did the user type a glob here” (expand it) or “a literal path” (use it as-is).

btv.glob.is_glob("src/*.rs")   --> true
btv.glob.is_glob("src/lib.rs") --> false

Defined in glob.lua.

btv.glob.split(list)

btv.glob.split(list) -> a list of patterns: split one comma-separated line of globs — the shape a “files to include” text box takes — trimming whitespace around each entry and dropping empty ones.

The comma is not a plain separator: {...} alternation puts commas inside a single glob, so only commas at brace depth zero split. That is the whole reason to call this rather than a hand-rolled gmatch.

btv.glob.split("src/**, docs/**")   --> { "src/**", "docs/**" }
btv.glob.split("**/{a,b}/**")       --> { "**/{a,b}/**" }   ( one pattern )
btv.glob.split("  , *.lock ,, ")    --> { "*.lock" }

Defined in glob.lua.