/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/glob/v0
gno.land/p/moul/x/daily/glob/v0
Shell-style pattern matching, with and without path awareness: Match,
MatchPath, Valid, Filter, FilterPath, Any, AnyPath.
1import "gno.land/p/moul/x/daily/glob/v0"
2
3glob.Match("*.gno", "a.gno") // true
4glob.Match("g1[a-z0-9]*", "g1manfred") // true
5glob.MatchPath("gno.land/r/moul/*", "gno.land/r/moul/home") // true
6glob.MatchPath("gno.land/r/moul/*", "gno.land/r/moul/x/daily") // false, '*' stops at '/'
7glob.MatchPath("gno.land/r/moul/**", "gno.land/r/moul/x/daily") // true
8
9allow := []string{"gno.land/r/moul/home", "gno.land/r/moul/x/daily/**"}
10glob.AnyPath(allow, "gno.land/r/moul/x/daily/globdemo") // true
11glob.AnyPath(allow, "gno.land/r/moul/homeless") // false
Live demo: r/moul/x/daily/globdemo.
Why not strings.HasPrefix
Every realm that decides what a caller may touch eventually writes an
allowlist, and the first version of it is a prefix check. The prefix
gno.land/r/moul/home also matches gno.land/r/moul/homeless; the prefix a
caller gets by trimming /** off gno.land/r/moul/x/daily/** also matches
gno.land/r/moul/x/dailyevil/drain. Registering a sibling name one byte longer
than an allowed one is cheap, and the check cannot tell the difference, because
a prefix has no idea where a path segment ends.
Appending the separator by hand is not the fix, it is the other half of the
same bug: gno.land/r/moul/home/ no longer matches the one path the rule was
written to name, and gno.land/r/moul/ no longer matches gno.land/r/moul.
The boundary has to be something the matcher knows about, not something every
call site remembers.
MatchPath makes the boundary part of the grammar rather than something the
caller remembered to append. *, ? and a character class stay inside one
segment; only a ** that is a whole segment crosses, and it crosses as many as
it needs, including zero (a/**/b matches a/b).
The grammar
? |
any single byte |
* |
any run of bytes, including none |
[abc] |
any one of the listed bytes |
[a-z] |
any byte in the range, both ends included |
[!abc] |
any byte not listed; [^abc] is the same thing |
\x |
the literal byte x, for any x |
** |
in MatchPath only, and only as a whole segment: zero or more segments |
A ] inside a class is written \]. There is no POSIX leading-] rule: one
way to say it beats two, and the one that survives also works for - and \.
A - at either end of a class is a literal dash, as POSIX has it.
Three decisions worth knowing before you use it
- An invalid pattern matches nothing.
Matchhas no error return, so the choice is between inert and permissive, and an allowlist entry with a typo that matches everything is the worse of the two.Validis how a caller finds out, and it should be called at write time, not at match time.Anyskips a bad entry and keeps evaluating the good ones, so one typo costs one rule rather than the whole list. - The matcher is a loop, not a recursion, and that is a security property.
The recursive formulation every glob tutorial shows branches at each
*and is exponential in the number of them:a*a*a*a*a*a*bagainst two dozenas is where it stops returning. This one walks forward and remembers a single backtrack point, the most recent star, which isO(len(pattern) * len(name))in time and constant in space. On chain the difference is not a slow page, it is a gas bomb a caller gets to aim, because the pattern is usually what the realm stored and the subject is usually what the caller supplied. There is no length cap for the same reason: the bound is a product of two lengths the caller already pays for. - It matches bytes, not runes.
?consumes one byte, so it takes two of them to cover a singleé, and a class range compares byte values. That is fine for what this is for, package paths, bech32 addresses, handles and file names, all ASCII. It is not the package for matching prose, and a[a-z]class will not do what you want to a non-ASCII name.
The separator belongs to the grammar, so the split has to know about it
In MatchPath a / that is escaped (a\/b) or that sits inside a character
class ([/]) is part of its segment and does not split the pattern. Splitting
there would cut an escape pair or a bracket expression in half and hand the
matcher a piece it would read past the end of, which is an abort out of a
Render that can never be redeployed. Such a pattern matches nothing, because
a name segment can never contain a separator, and it says so by returning
false. A negated class is the exception worth knowing: [!/] is a perfectly
ordinary one-segment pattern meaning any byte but a separator.
What it is not
Not a regular expression engine, and not a path cleaner: it compares the
strings it is handed. a//b has an empty segment and is not a/b, a trailing
/ leaves a trailing empty segment, and . and .. mean nothing. Normalise
before matching if your input can contain any of those.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.