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")isfalse; say"**/*.lua", or passbasename = trueto match the path’s last component. This is shell / gitignore / LSP semantics. Passliteral_separator = falsefor 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 passstyle = "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.