Regular Expressions Cheatsheet

Lookahead and Lookbehind

Use this Regular Expressions reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The four forms

Lookaround is zero-width. It tests what surrounds the current position and consumes nothing, so the assertion never lands in the match text.

FormNameAsserts
(?=...)positive lookaheadwhat follows matches
(?!...)negative lookaheadwhat follows does not match
(?<=...)positive lookbehindwhat precedes matches
(?<!...)negative lookbehindwhat precedes does not match
import re
re.findall(r'\d+(?= USD)', '10 USD 20 EUR 30 USD')   # ['10', '30']
re.findall(r'\d+(?! USD)', '10 USD 20 EUR')          # ['1', '20']
re.findall(r'(?<=\$)\d+', 'cost $42 and 7')          # ['42']
re.findall(r'(?<!\$)\b\d+\b', 'cost $42 and 7')      # ['7']

The second line is the classic surprise. \d+(?! USD) matched 1 because after consuming just 1 the next characters are 0 USD, which is not USD. Negative lookahead needs an anchor or boundary next to it: \d+\b(?! USD).

Match not followed by something

foo(?!bar)          foo not followed by bar
\bfoo\b(?!\s*\()    foo that is not a function call
^(?!#)\S+           a line that does not start with #
^(?!.*DEBUG).*$     a whole line containing no DEBUG
\.(?!\d)            a period that is not a decimal point
# every line that does not contain "healthz"
grep -P '^(?!.*healthz)' access.log
# or, clearer and portable
grep -v healthz access.log

Use grep -v for a whole-line exclusion. Reach for (?!...) when the negation is only part of a larger pattern, such as "a line with a status code that is not 2xx".

Password validation, worked

Stack independent lookaheads at the start. Each one tests the whole remaining string from position 0 and then hands the position back untouched, so they compose freely and their order does not matter.

^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])\S{10,72}$
PieceRequires
^start of string
(?=.*[a-z])at least one lowercase letter
(?=.*[A-Z])at least one uppercase letter
(?=.*\d)at least one digit
(?=.*[^A-Za-z0-9])at least one symbol
\S{10,72}10 to 72 non-space characters, this is the part that matches
$end of string
pw = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)'
                r'(?=.*[^A-Za-z0-9])\S{10,72}$')
bool(pw.match('Passw0rd!x'))    # True
bool(pw.match('password!x1'))   # False, no uppercase
bool(pw.match('Pw0rd!xx'))      # False, too short

In real code prefer separate checks per rule, because then you can tell the user which rule failed instead of only that the password is invalid.

Lookaround for insert-only substitutions

Because lookaround consumes nothing, a replacement can insert at a position without rewriting the characters around it.

// thousands separators, right to left
"1234567".replace(/\B(?=(\d{3})+$)/g, ",");   // 1,234,567
# trim both ends without touching the middle
re.sub(r'^\s+|\s+$', '', '  hi  ')            # 'hi'
# comment out lines that are not already commented
re.sub(r'^(?!#)', '# ', text, flags=re.M)

PCRE has a shortcut for the same idea. \K drops everything matched so far out of the reported match, so it acts as a variable-length lookbehind.

echo "key: value" | grep -Po 'key: \K.*'   # value

Lookbehind width limits per flavor

This is the one portability wall people hit.

FlavorLookbehind
JavaScriptunbounded, (?<=ab*) is fine
.NETunbounded
Python refixed width only, even (?<=a|bb) is a compile error
Python regex packageunbounded
PCRE2fixed width, or alternatives of differing fixed widths, or {n,m}. No + or *. Use \K instead
Javabounded, {1,3} fine, and it accepts + and * in practice
Rubyfixed width, or fixed-width alternatives
Go RE2no lookaround at all, in either direction
re.compile(r'(?<=ab)c')     # ok
re.compile(r'(?<=ab+)c')    # error: look-behind requires fixed-width pattern
re.compile(r'(?<=a|bb)c')   # error, the branches differ in width

Workaround in Python: capture the prefix instead of asserting it, then read group 1. Workaround in Go: match the surrounding characters for real and take the capture group.

// Go RE2 has no (?<=\$), so match the $ and capture the number
regexp.MustCompile(`\$(\d+)`).FindStringSubmatch("cost $42")[1]  // "42"

Cost and gotchas

  • Lookaround does not backtrack into itself. Once an assertion succeeds the engine forgets how, so you cannot reference a group captured inside a lookahead from a later part of the pattern in every flavor (JavaScript and Python do keep it, .NET does too).
  • A group inside a negative lookaround never captures usefully, since the assertion only succeeds when its body failed.
  • (?=.*x) scans to the end of the string each time. Four stacked lookaheads on a long input is four full scans. Fine for a password field, wasteful in a loop over megabytes.
  • ^(?!.*X).*$ over a huge line is O(n) per starting position. Prefer a whole-line negation in the host language.
  • \b plus a negative lookahead is almost always what you meant. \d+(?!%) is a trap, \d+\b(?!%) is not.