Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

v0 source pure

Package glob matches shell-style patterns against strings, in two modes: plain, where \`\*\` crosses everything, and ...

Readme View source

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. Match has 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. Valid is how a caller finds out, and it should be called at write time, not at match time. Any skips 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*b against two dozen as is where it stops returning. This one walks forward and remembers a single backtrack point, the most recent star, which is O(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.

Overview

Package glob matches shell-style patterns against strings, in two modes: plain, where `*` crosses everything, and path, where `/` is a boundary that only `**` crosses.

The reason to have it on chain is the allowlist. A realm that decides what a caller may touch writes the rule as a pattern (gno.land/r/moul/**), and the obvious alternative, strings.HasPrefix, is wrong in a way that only shows up in production: the prefix gno.land/r/moul/home also matches gno.land/r/moul/homeless, and the prefix a caller gets by trimming /** off the rule above, gno.land/r/moul, also matches gno.land/r/moulier/drain, a different namespace owned by a different person. A separator-aware matcher cannot make either mistake, because a boundary is part of the grammar rather than something the caller remembered to append. Remembering to append it is no answer either: gno.land/r/moul/home with a slash on the end stops being a rule that matches the path it names.

The matcher is iterative with a single backtrack point, so it runs in O(len(pattern) * len(name)) time and constant space. That is not a style preference. The recursive one-liner every glob tutorial shows is exponential on a pattern like a*a*a*a*a*a*b, and a realm that accepts a pattern from a caller and runs an exponential matcher on it has handed out a gas bomb.

Matching is byte-oriented, deliberately: `?` matches one byte and a class range compares bytes, so this package is for ASCII subjects (package paths, bech32 addresses, handles, file names) and not for matching prose. See the package README for what that rules out.

A live demo of this package is at r/moul/x/daily/globdemo(/r/moul/x/daily/globdemo/v0).

Constants 1

Functions 7

func Any

1func Any(patterns []string, name string) bool
source

Any reports whether name matches at least one of patterns, using Match.

An invalid entry is skipped rather than failing the whole list: one typo in an allowlist should cost that rule and no other.

func AnyPath

1func AnyPath(patterns []string, name string) bool
source

AnyPath is Any with MatchPath semantics. This is the allowlist primitive: a list of rules, a subject, one boolean.

func Filter

1func Filter(pattern string, names []string) []string
source

Filter returns the elements of names matching pattern, in their original order, using Match.

The result is never nil, so a caller can range and len it with no guard.

func Match

1func Match(pattern, name string) bool
source

Match reports whether name matches pattern.

Example
1?          any single byte
2*          any sequence of bytes, including none
3[abc]      any one of the listed bytes
4[a-z]      any byte in the range, ends inclusive
5[!abc]     any byte NOT listed; [^abc] is the same thing
6\x         the literal byte x, for any x
7anything else  itself

`*` crosses every byte, including Separator. Use MatchPath when the subject is a path and a wildcard must stay inside one segment.

An invalid pattern matches nothing. There is no error return, so the choice is between inert and permissive, and a bad allowlist entry that matches everything is the worse of the two. Ask Valid when it matters.

func MatchPath

1func MatchPath(pattern, name string) bool
source

MatchPath reports whether name matches pattern, with Separator as a boundary neither side may cross by accident.

Both are split into segments, and then:

  • a segment that is exactly `**` matches zero or more whole segments
  • every other segment is matched by Match against one segment, so `*`, `?` and a class can never reach past the separator

`**` only crosses as a whole segment. Glued to anything else, as in `a**b`, it is an ordinary star confined to its segment.

A Separator that is escaped (`a\/b`) or inside a class (`[/]`) belongs to its segment and does not split the pattern. Such a pattern matches nothing, because a name segment can never contain a separator, and it says so by returning false rather than by aborting.

An invalid pattern matches nothing, as in Match.

func Valid

1func Valid(pattern string) bool
source

Valid reports whether pattern is well formed.

Three things make it malformed, and each of them is a typo a caller makes rather than a construct anyone writes on purpose: an unterminated class (`[a-z`), an empty one (`[]`, `[!]`), and a trailing backslash with nothing to escape (`abc\`).

Dependencies

Imports 1

/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/glob/v0

Imported by indexer

Source Files 4