Eternalsix

Regular expressions for everyday developers

You don't need to memorize the whole grammar. A dozen constructs cover almost every regex you'll ever write — here they are, with the traps.

Regular expressions have a reputation for being write-only — easy to produce, impossible to read six months later. That reputation is half-earned. A genuinely gnarly regex is hard to read. But the regexes you write day to day — validating input, extracting a value from a log line, find-and-replace across a file — draw from a surprisingly small vocabulary. Learn maybe a dozen constructs and you can read and write the vast majority of practical patterns with confidence.

This is that dozen, plus the handful of traps that cause real bugs.

The mental model

A regular expression is a tiny pattern language for describing sets of strings. The engine reads your input left to right and tries to match the pattern. Most characters match themselves: the pattern cat matches the letters c-a-t in sequence. The power comes from metacharacters — symbols that mean "any," "one or more," "start of line," and so on.

Character classes: matching a kind of character

Instead of a literal character, you often want "any digit" or "any letter." That's a character class.

So \d\d:\d\d matches a time like 09:30, and [A-Za-z]+ matches a run of letters.

Quantifiers: how many times

Quantifiers say how many of the preceding thing to match.

\d{3}-\d{4} matches 555-1234. colou?r matches both color and colour — the ? makes the u optional.

Anchors: where in the string

Anchors match positions, not characters.

The difference between \d+ and ^\d+$ is enormous: the first matches any digits anywhere in the input (so it happily matches abc123), while ^\d+$ requires the entire string to be digits. When you're validating input, forgetting the anchors is the number-one reason a pattern "passes" strings it shouldn't. \bcat\b matches the word cat but not the cat inside category — word boundaries are how you avoid matching substrings.

Groups and alternation

Parentheses ( ) group part of a pattern, both so a quantifier can apply to the whole group and so you can capture what matched for later use.

Captured groups are how you extract data, not just test for a match. In a replacement, you refer back to them as $1, $2 (or \1, \2 depending on the tool), so you can reformat 2026-07-03 into 07/03/2026 in one substitution.

There's also a non-capturing group, (?:...), for when you need grouping but don't want to capture — useful for performance and for keeping your group numbers meaningful.

The traps that cause real bugs

Knowing the syntax isn't enough; a few behaviors reliably surprise people.

Greedy vs. lazy matching

Quantifiers are greedy by default — they match as much as possible. Given the input <a><b> and the pattern <.*>, you might expect it to match <a>. It doesn't — .* greedily grabs everything up to the last >, matching the whole <a><b>. Add a ? after the quantifier to make it lazy (match as little as possible): <.*?> matches just <a>. This one behavior is behind a huge share of "my regex matches too much" bugs.

Escaping metacharacters

To match a literal ., ?, *, (, $, or other metacharacter, escape it with a backslash. A dot in example.com is example\.com — unescaped, that . would match any character, so exampleXcom would pass. When you're matching things like file names, IP addresses, or money amounts, unescaped dots are a classic source of false matches.

Catastrophic backtracking (ReDoS)

Certain patterns, on certain inputs, can make the engine explore an exponential number of possibilities and effectively hang. The usual culprit is nested quantifiers over overlapping alternatives, like (a+)+$ fed a long string of as followed by a non-match. This isn't just a performance nuisance — it's a denial-of-service vector (ReDoS) when the pattern runs on untrusted input. If a regex validates user-supplied data, keep it simple, avoid nesting quantifiers, and prefer more specific character classes over broad .* runs.

Flags change everything

Most of a pattern's behavior can be toggled with flags:

Forgetting g on a replace is why "it only replaced the first one." Forgetting m is why ^ didn't match the start of each line.

A worked example

Suppose you want to validate a simple email-ish string. A pragmatic pattern:

^[\w.+-]+@[\w-]+\.[A-Za-z]{2,}$

Read it piece by piece: anchored at the start, one or more word/dot/plus/hyphen characters (the local part), an @, one or more word/hyphen characters (the domain), a literal dot, then at least two letters (the TLD), anchored at the end. It's not RFC-perfect — full email validation is famously near-impossible with regex alone — but it's readable, and it rejects the obvious garbage.

Notice how the pieces you just learned combine: character classes for the allowed characters, + quantifiers, an escaped \. for the literal dot, {2,} for the TLD length, and ^/$ anchors so the whole string must match.

Test, don't guess

The single best habit with regex is to test against real examples — both strings that should match and strings that shouldn't — rather than reasoning purely in your head. A Regex Tester lets you paste a pattern and a block of sample text and see exactly what matches, which groups captured what, and how a replacement would come out, in real time. Watching a greedy quantifier swallow more than you intended is far more instructive than any explanation.

The takeaway

You don't need the whole regex grammar. Character classes (\d, \w, [a-z]), quantifiers (*, +, ?, {n,m}), anchors (^, $, \b), groups, and alternation cover almost everything you'll write. The traps to respect are greedy matching (reach for lazy ? when it grabs too much), unescaped metacharacters (especially the dot), and catastrophic backtracking on untrusted input. Learn the dozen constructs, test against real strings, and regex becomes a precise tool instead of a dark art.