gex
April 28, 2026 · View on GitHub
"When regular expressions are just too hard!"
Glob expressions for JavaScript / TypeScript. * matches any run of
characters, ? matches one character, ** and *? escape literal
* and ?. Patterns are anchored — they must match the whole string.
This README covers the JavaScript / TypeScript package. A Go port lives
in go/ — see go/README.md for installation
and API. Matching semantics are the same in both ports.
Tutorial: your first gex
Install:
npm install gex
Build a matcher and try it:
const { Gex } = require('gex')
Gex('a*c').on('abbbc') // 'abbbc' — match, returns the input
Gex('a?c').on('abc') // 'abc' — match
Gex('a*c').on('xyz') // null — no match
on() returns the input when it matches, or null when it doesn't.
Two convenient extensions cover collections:
Gex('a*').on(['ab', 'zz', 'ac']) // ['ab', 'ac']
Gex('a*').on({ ab: 1, zz: 2, ac: 3 }) // { ab: 1, ac: 3 }
A Gex can hold several specs; a value matches if any spec matches:
Gex(['a*', 'b*']).on('bx') // 'bx'
Gex(['a*', 'b*']).on(['ax', 'zz', 'bx']) // ['ax', 'bx']
That's the whole library.
How-to guides
Filter a list of files
const fs = require('fs')
fs.readdir('.', (err, files) => {
const pngs = Gex('*.png').on(files)
})
Filter an object's keys
Gex('foo*').on({ foo: 1, doo: 2, food: 3 })
// { foo: 1, food: 3 }
Property values are copied by reference. The traversal does not recurse into nested objects or arrays.
Make a fuzzy assertion in a test
When a value has fields that are noisy in tests (timestamps, random ids), pattern-match the JSON form:
const entity = { created: Date.now(), name: 'foo' }
assert.ok(Gex('{"created":*,"name":"foo"}').on(JSON.stringify(entity)))
Combine several patterns
Gex(['*.png', '*.jpg']).on(files)
A value matches if any of the supplied specs match. Specs are tried in array order; the first match wins.
Escape literal * or ?
Gex('a**b').on('a*b') // 'a*b' — '**' is a literal '*'
Gex('a*?b').on('a?b') // 'a?b' — '*?' is a literal '?'
g.esc(s) doubles * to ** and ? to *? for you, so user-supplied
text can be embedded safely:
Gex('').esc('a*b?c') // 'a**b*?c'
Inspect the compiled regex
Gex('a*b').re() // /^a[\s\S]*b$/
Gex(['a', 'b']).re() // { a: /^a$/, b: /^b$/ }
Gex('a*').toString() // 'Gex[a*]'
Reference
Gex(spec)
Construct a Gex. spec is one of:
| Type | Treated as |
|---|---|
string | one glob spec |
string[] | several glob specs (any-of) |
number / boolean / Date / RegExp | stringified, then one spec |
null / undefined / NaN | a Gex that never matches |
.on(value)
| Input | Returns |
|---|---|
string / number / boolean / Date / RegExp | the input if its string form matches, else null |
| array | new array of matching elements (not recursive) |
| object | new object with entries whose keys match |
null / undefined / NaN | null |
.match(value)
The boolean form of .on() for scalars: true if the value's string
form matches any spec, otherwise false.
.esc(s)
Escape * and ? so the result, used as a spec, matches the input
literally.
.re()
Returns the compiled RegExp if the Gex has a single spec, or the
{ spec: RegExp } map otherwise.
.toString() / .inspect()
Render as Gex[spec1,spec2,...].
Explanation
Why a separate library when JS has regex? Glob syntax is shorter,
easier to read at a glance, and easier to assemble from user-supplied
input than a regex. gex is a thin compiler from glob to anchored
regex plus a small filtering API for arrays and objects.
How the regex is built. Specs are anchored (^...$); * becomes
[\s\S]* and ? becomes [\s\S], so patterns cross newlines. **
and *? round-trip back to literal \* and \? after escaping, so
escaping composes correctly.
What .on() is for. It collapses three common shapes into one
call: "is this string a match?", "which of these strings match?", and
"which of these keys match?". The same Gex object handles all three.
Use cases the API is shaped around.
- Plugin name matching (
Gex('seneca-*')to recognise plugin packages). - Filtering filenames returned by
fs.readdir. - Test assertions on JSON snapshots where timestamps or UUIDs are
irrelevant — pattern-match those fields with
*.
Other languages. The Go port in go/ shares the same
matching semantics. The Go API differs where Go's type system makes a
different shape natural — see go/README.md for the
details.
License
Copyright (c) 2010-2026, Richard Rodger and other contributors. Licensed under MIT.