Programming

Regular Expressions, Properly

·2 min read

Regular expressions are simultaneously the most-used and most-misunderstood tool in a developer's belt. Half of every regex problem is knowing which flavour you're working in. The other half is internalizing six or seven core concepts that compose to cover almost everything.

Character classes

.        # any character except newline (unless /s flag)
[abc]    # one of a, b, c
[^abc]   # NOT one of a, b, c
[a-z]    # range
\d       # digit (in PCRE/Python; [0-9] in POSIX BRE)
\w       # word char [A-Za-z0-9_]
\s       # whitespace

Quantifiers

x*       # zero or more
x+       # one or more
x?       # zero or one
x{3}     # exactly 3
x{2,5}   # 2 to 5
x{2,}    # 2 or more

By default quantifiers are greedy — they match as much as possible. Append ? to make them lazy: .+? matches the shortest run. The classic mistake:

# Want to grab content between two tags?
<.+>       # greedy — matches "bold and italic" entirely
<.+?>      # lazy — matches "" then "" etc.

Anchors

^        # start of line (or string with /m off)
$        # end of line
\b       # word boundary
\A       # absolute start of input
\z       # absolute end

Groups and captures

(abc)        # capturing group #1
(?:abc)      # non-capturing group
(?Px)  # named group (Python) — (?x) in PCRE/JS
\1           # backreference to group 1

Lookarounds

The most powerful feature most people don't use. Lookarounds assert "this pattern is/isn't there" without consuming characters:

foo(?=bar)    # 'foo' only if followed by 'bar' (lookahead)
foo(?!bar)    # 'foo' only if NOT followed by 'bar'
(?<=bar)foo   # 'foo' only if preceded by 'bar' (lookbehind)
(?

Extracting a price from "Total: $42.50" without capturing the dollar sign:

(?<=\$)\d+\.\d{2}

The flavour zoo

Five rough families: POSIX BRE (basic — grep default), POSIX ERE (grep -E, sed -E), PCRE (Perl-compatible — grep -P, most languages), Python re, JavaScript RegExp. They mostly agree, but:

  • BRE requires \(, \+, \? for those metacharacters
  • \d works in PCRE/Python/JS, not POSIX BRE/ERE — use [0-9]
  • Lookbehind support varies in length restrictions (Python ≤ 3.6 required fixed-width; modern engines are fine)

Performance trap: catastrophic backtracking

Some patterns explode exponentially on adversarial input. The classic is nested quantifiers like (a+)+b against a long string of as. The fix is usually to anchor the pattern or use possessive quantifiers (a++) in engines that support them. If you ever see your regex hang for seconds, this is why.

Tools that help

  • regex101.com — explains every token in your pattern
  • regexper.com — railroad diagrams
  • grep -P --color=auto — quick local testing

Related