A regex engine for Go that can't be tricked into hanging.
Most regex engines — the ones in Python, JavaScript, Java, Ruby, PCRE — work by
backtracking. It's a clever trick, and it's why they can do fancy things like
backreferences. It's also why a pattern as harmless-looking as (a+)+$ can bring
a production server to its knees. Feed it the wrong string and it doesn't just
get slow, it gets exponentially slow. People have taken down real services this
way. It even has a name: ReDoS.
flare takes a different road. It doesn't backtrack at all. Under the hood it runs
your pattern as a state machine that walks the input exactly once, so matching is
always linear in the length of the text. There is no evil input. There is no
pattern that blows up. You can hand it a regex from a random user on the internet
and go to sleep.
Here's the same pattern, (a+)+$, given a string of as that doesn't quite
match:
Python's re → 30 characters → still running after 10 seconds ✗
flare → 100,000 chars → done in 0.02 seconds ✓
That's not a tuned benchmark. That's just what happens.
Install
go get github.com/umudhasanli/flare-regex
Go 1.21 or newer. No dependencies — it's the standard library and nothing else.
A quick taste
package main
import (
"fmt"
"github.com/umudhasanli/flare-regex"
)
func main() {
re := flare.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
fmt.Println(re.MatchString("today is 2026-07-10")) // true
fmt.Println(re.FindString("today is 2026-07-10")) // 2026-07-10
fmt.Println(re.FindStringSubmatch("2026-07-10")) // [2026-07-10 2026 07 10]
fmt.Println(re.ReplaceAllString("2026-07-10", "$3/$2/$1")) // 10/07/2026
}
If you've used Go's standard regexp package, this will feel like home. That's on
purpose — the method names line up so you can try flare without relearning
anything.
When raw regex gets ugly, build it in English
Nobody enjoys reading ^\d{1,3}(\.\d{1,3}){3}$ six months after they wrote it.
So flare ships a builder that reads like a sentence:
// Matches an IPv4 address like 192.168.0.1
re := flare.New().
Start().
Digit().Between(1, 3).
Group(func(b *flare.Builder) {
b.Lit(".").Digit().Between(1, 3)
}).Times(3).
End().
MustCompile()
re.MatchString("192.168.0.1") // true
Lit escapes metacharacters for you, so New().Lit("a.b") looks for the literal
text a.b — not "a, then anything, then b". One less thing to get wrong.
The feature Go's own regexp doesn't have: it explains itself
Everyone has stared at a regex someone else wrote and had no idea what it does.
flare can just tell you — in plain English:
re := flare.MustCompile(`^(cat|dog)s?\d+$`)
fmt.Print(re.Explain())
• the start of the text
• capturing group #1:
• one of:
• the text "cat"
• the text "dog"
• the character "s", optional (zero or one time)
• a digit (0-9), repeated one or more times
• the end of the text
The standard library can run your pattern, but it can't describe it. flare
keeps the parsed structure around, so it walks the pattern and turns it back into
readable steps. It works on the command line too:
flare -explain '(\d{4})-(\d{2})-(\d{2})'
Between the Builder (English → regex) and Explain (regex → English), you never
have to hold a cryptic pattern in your head again.
The CLI: grep that can't be DoS'd
Installing the module also gives you a little command-line tool. It behaves like
grep, but it's backed by the same engine, so no pattern anyone types will ever
hang it.
go install github.com/umudhasanli/flare-regex/cmd/flare@latest
flare '^error:' server.log # lines that match
flare -o '\d{3}-\d{4}' contacts.txt # only the matching parts
flare -n 'TODO' *.go # with line numbers
flare -c 'WARN' app.log # just count them
flare -r '$3/$2/$1' '(\d{4})-(\d{2})-(\d{2})' dates.txt # find and replace
cat access.log | flare '4\d\d' # reads stdin too
What it understands
|
|
| Literals |
abc |
| Any character |
. |
| Character classes |
[a-z0-9], [^aeiou] |
| Shorthands |
\d \D \w \W \s \S |
| Anchors |
^ and $ |
| Alternation |
cat|dog|bird |
| Groups |
(...) and (?:...) |
| Repetition |
* + ? {3} {2,} {2,5} |
| Lazy repetition |
*? +? ?? |
| Escapes |
\. \* \n \t … |
What it deliberately leaves out
Backreferences (\1), lookahead/lookbehind ((?=...)), and named groups.
This isn't laziness — it's the whole point. Those are exactly the features that
force an engine to backtrack, and backtracking is the thing that makes regex
dangerous. Leaving them out is what lets flare promise it will never hang. It's a
trade, and for the huge majority of real-world patterns, it's a trade worth
making.
How it works, briefly
Three steps, and the whole engine is small enough to read in an afternoon:
- A recursive-descent parser turns your pattern into a small syntax tree.
- A compiler flattens that tree into a list of simple instructions — every
*, +, or | becomes a branch.
- A virtual machine runs all the possible branches at the same time, keeping a
set of active states and advancing them one character at a time.
Because the set of active states can never be bigger than the program itself, the
work per character is bounded. Multiply by the number of characters and you get
linear time. There's simply no room for an explosion to happen.
This is the same idea behind Go's own regexp package and Google's RE2, described
beautifully in Russ Cox's articles on the subject.
flare is a small, readable, dependency-free take on it.
Contributing
This project is meant to be approachable. If you've ever wanted to understand how
a regex engine actually works, this is a good place to poke around — the code is
short and there's no magic hiding in a dependency.
Good first contributions:
- More of the syntax (Unicode classes,
\b word boundaries, POSIX classes)
- A
[]byte API to skip the string conversion in hot paths
- Fuzz tests that check flare agrees with the standard library on shared syntax
- More CLI flags (
-i for case-insensitive, recursive directory search)
Open an issue to talk it through, or just send a PR. All welcome.
License
MIT. Do what you like with it — see LICENSE.
If flare saved you from a regex that hangs, consider giving it a ⭐.
It genuinely helps other people find it.