Brings Bash-style globbing to Go for filesystem matching like recursive **, extended patterns, case-insensitive searches, ignore rules, and more. All opt-in for flexibility.
go get go.dw1.io/richglob
package main
import (
"fmt"
"path/filepath"
"go.dw1.io/richglob"
)
func main() {
matched, err := richglob.Match(
filepath.FromSlash("src/**/main.go"),
filepath.FromSlash("src/cmd/tool/main.go"),
richglob.WithGlobStar(),
)
if err != nil {
panic(err)
}
println(matched)
}
matches, err := richglob.Glob("src/**/*.go", richglob.WithGlobStar())
if err != nil {
panic(err)
}
for _, match := range matches {
println(match)
}
for match, err := range richglob.GlobSeq2("src/**/*.go", richglob.WithGlobStar()) {
if err != nil {
panic(err)
}
println(match)
}
By default, richglob supports the usual glob operators:
*matches any sequence of non-separator characters?matches any single non-separator character[abc]matches a character class[^abc]matches any character outside a class\escapes metacharacters on non-Windows platforms
Patterns are path-aware. * and ? do not cross path separators.
WithGlobStar()enables**to match zero or more directory segments.WithExtGlob()enables extglob operators:?(...),*(...),+(...),@(...),!(...).WithNoCaseGlob()makes matching case-insensitive.WithDotGlob()includes hidden entries when Bash pathname rules are active.WithGitIgnore()auto-discovers.gitignorefiles while walking and applies them as traversal-time filters.WithGlobSkipDots(true)skips.and..during Bash-style expansion.WithGlobASCIIRanges()makes ranges such as[A-Z]use ASCII ordering.WithGlobIgnore(patterns...)filtersGlobresults through ignore patterns.WithSort(richglob.SortNone)keeps filesystem traversal order.WithNullGlob()returns an empty, non-nil slice when nothing matches.WithFailGlob()returnsErrNoMatchwhen nothing matches.
Note
Matchchecks a single path or path segment string against a pattern.Globwalks the filesystem and returns matching paths.GlobSeq2walks the filesystem lazily and yields(path, error)pairs.Globsorts results lexicographically by default.GlobSeq2yields matches in traversal order and does not globally sort before yielding.- If
Globfinds nothing, it returnsnil, nilby default. GlobSeq2yields a terminal error pair for malformed patterns, invalid ignore patterns, orWithFailGlob().WithFailGlob()takes precedence overWithNullGlob().- Hidden files are included by default. When Bash-style pathname rules are enabled, hidden entries are excluded unless
WithDotGlob()is also set.WithGlobIgnore(...)opts into Bash-style hidden-file handling on its own, whileWithGitIgnore()keeps hidden entries filtered unlessWithDotGlob()is also enabled. - When
WithGitIgnore()andWithGlobIgnore(...)are both set,.gitignorerules apply first during traversal andWithGlobIgnore(...)acts as an additional result filter.
Case-insensitive matching:
ok, err := richglob.Match("*.GO", "main.go", richglob.WithNoCaseGlob())
Recursive search with **:
matches, err := richglob.Glob("src/**/*.go", richglob.WithGlobStar())
Lazy recursive search:
for match, err := range richglob.GlobSeq2("src/**/*.go", richglob.WithGlobStar()) {
if err != nil {
panic(err)
}
println(match)
}
Extglob alternation:
ok, err := richglob.Match("@(main|util).go", "util.go", richglob.WithExtGlob())
Ignore generated files:
matches, err := richglob.Glob(
"src/**/*.go",
richglob.WithGlobStar(),
richglob.WithGlobIgnore("src/**/*_generated.go"),
)
Respect nested .gitignore files while walking:
matches, err := richglob.Glob(
"src/**/*.go",
richglob.WithGlobStar(),
richglob.WithGitIgnore(),
)
Compared against the doublestar and the std library's filepath.{Match,Glob}.
benchstat
goos: linux
goarch: amd64
pkg: benchmarks
cpu: AMD EPYC 7763 64-Core Processor
│ richglob │ doublestar │ std │
│ sec/op │ sec/op vs base │ sec/op vs base │
Match-4 80.05n ± 3% 108.70n ± 0% +35.80% (p=0.000 n=10) 106.65n ± 0% +33.24% (p=0.000 n=10)
Match/recursive-4 129.4n ± 0% 105.7n ± 0% -18.28% (p=0.000 n=10)
Glob-4 13.98µ ± 1% 13.74µ ± 1% -1.70% (p=0.000 n=10) 17.55µ ± 1% +25.54% (p=0.000 n=10)
Glob/recursive-4 90.50µ ± 1% 162.24µ ± 1% +79.26% (p=0.000 n=10)
geomean 1.902µ 2.250µ +18.25% 1.368µ +29.33% ¹
¹ benchmark set differs from baseline; geomeans may not be comparable
│ richglob │ doublestar │ std │
│ B/op │ B/op vs base │ B/op vs base │
Match-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ 0.000 ± 0% ~ (p=1.000 n=10) ¹
Match/recursive-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Glob-4 1.495Ki ± 0% 1.872Ki ± 0% +25.21% (p=0.000 n=10) 1.026Ki ± 0% -31.35% (p=0.000 n=10)
Glob/recursive-4 8.115Ki ± 0% 11.178Ki ± 0% +37.74% (p=0.000 n=10)
geomean ² +14.60% ² -17.15% ³ ²
¹ all samples are equal
² summaries must be >0 to compute geomean
³ benchmark set differs from baseline; geomeans may not be comparable
│ richglob │ doublestar │ std │
│ allocs/op │ allocs/op vs base │ allocs/op vs base │
Match-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ 0.000 ± 0% ~ (p=1.000 n=10) ¹
Match/recursive-4 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Glob-4 31.00 ± 0% 53.00 ± 0% +70.97% (p=0.000 n=10) 24.00 ± 0% -22.58% (p=0.000 n=10)
Glob/recursive-4 152.0 ± 0% 273.0 ± 0% +79.61% (p=0.000 n=10)
geomean ² +32.38% ² -12.01% ³ ²
¹ all samples are equal
² summaries must be >0 to compute geomean
³ benchmark set differs from baseline; geomeans may not be comparable
Highlights:
- richglob outperforms doublestar by 35-36% in
Matchops and is 25% faster than the standard library'sGlob. - For recursive globbing (
**), richglob is 79% faster than doublestar. - richglob uses fewer memory allocs (31 vs 53 for doublestar) and less memory in
Globops. - Overall geomean perf shows richglob is 18% faster than doublestar and 29% faster than std.
Run benchmarks yourself:
make -C benchmarks/
richglob is released with ♡ by @dwisiswant0 under the Apache 2.0 license. See LICENSE.