flare

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 5 Imported by: 0

README

flare

A regex engine for Go that can't be tricked into hanging.

CI Go Reference Go Report Card MIT License Zero dependencies


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.

Named groups, case-insensitivity, and bytes

Real patterns need real features. Name your groups and pull them out by name:

re := flare.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
m := re.FindStringSubmatchMap("2026-07-10")
// map[year:2026 month:07 day:10]

Match without caring about case, either for the whole pattern or just part of it:

flare.MustCompile(`(?i)hello`).MatchString("HELLO")   // true
flare.MustCompile(`(?i:cat)dog`).MatchString("CATdog") // true

And when you're working with []byte, skip the string conversion entirely — Match, Find, FindSubmatch, FindAll, and ReplaceAll all take bytes.

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 ^, $, \b, \B
Alternation cat|dog|bird
Groups (...), (?:...), (?P<name>...)
Case-insensitive (?i) and (?i:...)
Repetition * + ? {3} {2,} {2,5}
Lazy repetition *? +? ??
Escapes \. \* \n \t

What it deliberately leaves out

Backreferences (\1) and lookahead/lookbehind ((?=...)).

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:

  1. A recursive-descent parser turns your pattern into a small syntax tree.
  2. A compiler flattens that tree into a list of simple instructions — every *, +, or | becomes a branch.
  3. 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.

Correctness and performance — the honest version

Correctness: flare is fuzz-tested against Go's standard regexp on a set of shared patterns. It's checked over tens of thousands of random inputs that both engines agree on every match. Run it yourself:

go test -fuzz FuzzAgainstStdlib -fuzztime 30s

Performance: be clear-eyed here. Go's standard library regexp is also a linear-time engine, and on ordinary patterns it is faster than flare and does zero allocations. flare converts to runes and tracks submatch state, so it trades some speed for its features. If you're on Go and want raw throughput on trusted patterns, use the standard library.

So when should you reach for flare? When you value what it adds: the readable Builder, Explain(), named-group maps, and a small self-contained engine you can actually read and learn from. The linear-time guarantee matters most when you're comparing against the backtracking engines in other languages — that's where "can't be DoS'd" is a feature you don't otherwise get.

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 syntax: Unicode character classes, POSIX classes ([[:alpha:]]), \A/\z
  • A UTF-8 fast path that avoids the []rune conversion (a real speed win)
  • Reducing per-match allocations in the VM
  • 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.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func QuoteMeta

func QuoteMeta(s string) string

Types

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

func New

func New() *Builder

func (*Builder) AnyChar

func (b *Builder) AnyChar() *Builder

func (*Builder) AnyOf

func (b *Builder) AnyOf(chars string) *Builder

func (*Builder) AtLeast

func (b *Builder) AtLeast(n int) *Builder

func (*Builder) Between

func (b *Builder) Between(min, max int) *Builder

func (*Builder) Compile

func (b *Builder) Compile() (*Regexp, error)

func (*Builder) Digit

func (b *Builder) Digit() *Builder

func (*Builder) End

func (b *Builder) End() *Builder

func (*Builder) Group

func (b *Builder) Group(fn func(*Builder)) *Builder

func (*Builder) Lazy

func (b *Builder) Lazy() *Builder

func (*Builder) Lit

func (b *Builder) Lit(s string) *Builder

func (*Builder) MustCompile

func (b *Builder) MustCompile() *Regexp

func (*Builder) NoneOf

func (b *Builder) NoneOf(chars string) *Builder

func (*Builder) OneOf

func (b *Builder) OneOf(options ...string) *Builder

func (*Builder) OneOrMore

func (b *Builder) OneOrMore() *Builder

func (*Builder) Optional

func (b *Builder) Optional() *Builder

func (*Builder) Range

func (b *Builder) Range(lo, hi rune) *Builder

func (*Builder) Raw

func (b *Builder) Raw(fragment string) *Builder

func (*Builder) Space

func (b *Builder) Space() *Builder

func (*Builder) Start

func (b *Builder) Start() *Builder

func (*Builder) String

func (b *Builder) String() string

func (*Builder) Times

func (b *Builder) Times(n int) *Builder

func (*Builder) Word

func (b *Builder) Word() *Builder

func (*Builder) ZeroOrMore

func (b *Builder) ZeroOrMore() *Builder

type Regexp

type Regexp struct {
	// contains filtered or unexported fields
}

func Compile

func Compile(pattern string) (*Regexp, error)

func MustCompile

func MustCompile(pattern string) *Regexp

func (*Regexp) Explain

func (re *Regexp) Explain() string

func (*Regexp) Find added in v0.2.0

func (re *Regexp) Find(b []byte) []byte

func (*Regexp) FindAll added in v0.2.0

func (re *Regexp) FindAll(b []byte, n int) [][]byte

func (*Regexp) FindAllString

func (re *Regexp) FindAllString(s string, n int) []string

func (*Regexp) FindString

func (re *Regexp) FindString(s string) string

func (*Regexp) FindStringIndex

func (re *Regexp) FindStringIndex(s string) []int

func (*Regexp) FindStringSubmatch

func (re *Regexp) FindStringSubmatch(s string) []string

func (*Regexp) FindStringSubmatchMap added in v0.2.0

func (re *Regexp) FindStringSubmatchMap(s string) map[string]string

func (*Regexp) FindSubmatch added in v0.2.0

func (re *Regexp) FindSubmatch(b []byte) [][]byte

func (*Regexp) Match added in v0.2.0

func (re *Regexp) Match(b []byte) bool

func (*Regexp) MatchString

func (re *Regexp) MatchString(s string) bool

func (*Regexp) NumSubexp

func (re *Regexp) NumSubexp() int

func (*Regexp) ReplaceAll added in v0.2.0

func (re *Regexp) ReplaceAll(src, repl []byte) []byte

func (*Regexp) ReplaceAllString

func (re *Regexp) ReplaceAllString(s, repl string) string

func (*Regexp) String

func (re *Regexp) String() string

func (*Regexp) SubexpNames added in v0.2.0

func (re *Regexp) SubexpNames() []string

Directories

Path Synopsis
cmd
flare command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL