Regex cheat sheet
A reference sorted by what you are looking for, with a column per token saying whether JavaScript, PCRE and Python agree. Many of them do not.
Most regex cheat sheets tell you what a token means. That is the easy half. The hard half — the half that costs an afternoon — is that a token can mean one thing where you read it and something else where you paste it, and almost no reference says which.
So every table here carries three extra columns: JS for the engine in your browser and in
Node, PCRE for the one inside PHP, Perl and a hundred embedded tools, and Py for
Python’s re module. Yes means the token exists and behaves as the row describes. A dash
means it does not, and the paragraph under the table will usually say what happens instead —
because the worst outcomes are not errors. Several of these tokens compile perfectly in an
engine that has never heard of them, and quietly match the wrong thing.
Everything below was run before it was written down. Where the engines disagree, the disagreement was measured rather than remembered.
Characters and character classes
| Token | Means | JS | PCRE | Py |
|---|---|---|---|---|
. |
Any character except a line break | yes | yes | yes |
[abc] |
One of these characters | yes | yes | yes |
[^abc] |
Any character except these | yes | yes | yes |
[a-z] |
A range | yes | yes | yes |
\d \D |
A digit, and anything but | ASCII | ASCII | Unicode |
\w \W |
Letter, digit or underscore | ASCII | ASCII | Unicode |
\s \S |
Whitespace, and anything but | yes | yes | yes |
\s on a no-break space |
Is U+00A0 whitespace? | yes | only with u |
yes |
\p{L} |
Any letter in any script | needs u |
yes | — |
[[:alpha:]] |
POSIX named class | — | yes | — |
\t \n \r |
Tab, line feed, carriage return | yes | yes | yes |
\x41 and \u0041 |
A character by code point | yes | yes | yes |
| Beyond U+FFFF | An emoji or a rare script | \u{1F600} with u |
\x{1F600} |
\U0001F600 |
Two rows in that table are responsible for a great deal of misery. The first is \d and
\w: in JavaScript and in PCRE they are strictly ASCII, so \d does not match the
Arabic-Indic digit ٣ and \w does not match é. In Python 3 both are Unicode-aware by
default, so both match. A pattern lifted from a Python answer on a forum and dropped into a
form validator therefore gets stricter on the way across, and only for users whose names
and numbers are not in the Latin alphabet.
The second is \p{L}. Python’s standard library has no property escapes at all — \p there
is a hard error — and JavaScript has them only when the u or v flag is on. Without the
flag, \p{L} in JavaScript is not an error either. It is five literal characters, and the
pattern that was supposed to accept every alphabet on earth accepts the exact text
p{L} and nothing else.
Anchors and word boundaries
| Token | Means | JS | PCRE | Py |
|---|---|---|---|---|
^ |
Start of the string, or of a line under m |
yes | yes | yes |
$ |
End of the string, or of a line under m |
yes | yes | yes |
\b \B |
A word boundary, and the absence of one | yes | yes | yes |
\A |
Start of the subject, ignoring m |
— | yes | yes |
\z |
Very end of the subject | — | yes | — |
\Z |
End, tolerating one trailing newline | — | yes | see below |
\G |
Where the previous match ended | — | yes | — |
\Z is the trap in that table, and it is a trap even for people who know both languages.
In PCRE, \z is the absolute end and \Z allows one trailing newline — /abc\Z/ matches
"abc\n" and /abc\z/ does not. Python has no \z at all, and its \Z means what PCRE
calls \z: strict, no trailing newline tolerated. So the same three characters describe two
different positions depending on which manual you last read, and a pattern that trims log
lines correctly in one is off by a newline in the other.
JavaScript has none of the four, and does not complain about any of them. An escape it does
not recognize keeps the character and throws the backslash away, so each of those tokens
degrades into an ordinary letter. Take a validation pattern anchored with \A and \z, drop
it into a browser, and the anchors become the capital A and the lowercase z — a pattern that
now demands two letters nobody typed and no longer restricts anything about where the match
sits. Run \A\d{3}\z against A300z and it succeeds, which is the worst possible outcome:
a green result for a pattern that has stopped doing its job.
Word boundaries deserve one warning of their own. \b in JavaScript and PCRE is defined
against the ASCII word characters, so it sees a boundary in the middle of an accented word.
/\bcafé\b/ does not match café. — the engine finds a boundary between f and é, and
the assertion fails where a human sees one unbroken word.
Quantifiers
| Token | Means | JS | PCRE | Py |
|---|---|---|---|---|
* + ? |
Zero or more, one or more, optional | yes | yes | yes |
{2} {2,} {2,5} |
Exactly, at least, between | yes | yes | yes |
*? +? ?? |
The lazy forms — as few as possible | yes | yes | yes |
*+ ++ |
The possessive forms — no backtracking | — | yes | 3.11+ |
(?>…) |
Atomic group, same idea around a group | — | yes | 3.11+ |
Greedy against lazy is the single most useful line on any cheat sheet, so here it is
concretely. Against <a><b>, the greedy <.+> returns <a><b> — one match spanning both
tags, because .+ takes everything it can and gives back only enough to let > succeed.
The lazy <.+?> returns <a>, because it takes as little as possible and stops at the first
> that works.
The possessive and atomic forms are how you tell an engine never to give anything back, and
they are the standard cure for a pattern that hangs. JavaScript has no cure, which matters
more than it sounds: new RegExp('a++') there does not quietly do something else, it throws
Nothing to repeat, and (?>…) throws Invalid group. Those two messages are perfectly
clear once you know what happened and completely opaque before.
Groups, captures and backreferences
| Token | Means | JS | PCRE | Py |
|---|---|---|---|---|
(…) |
Capture, numbered from 1 | yes | yes | yes |
(?:…) |
Group without capturing | yes | yes | yes |
(?<name>…) |
Named capture | yes | yes | — |
(?P<name>…) |
Named capture, Python spelling | — | yes | yes |
\1 |
Backreference by number | yes | yes | yes |
\k<name> |
Backreference by name | yes | yes | — |
(?P=name) |
Backreference by name, Python spelling | — | yes | yes |
(?#…) |
Inline comment | — | yes | yes |
PCRE accepts both spellings of a named group, which is why so much borrowed code carries the
Python one. Handed to JavaScript, (?P<name>…) throws Invalid group — a rare case where
the difference is loud rather than silent, and therefore cheap.
One behavior no table can show: absent and blank are different outcomes. Matching (a)|(b)
against b yields the match b, group one null, group two b — the first group exists in
the pattern and simply never ran. Treat those two states as interchangeable and the resulting
mistake hides on whichever alternative your fixtures happen not to exercise.
Lookaround
| Token | Means | JS | PCRE | Py |
|---|---|---|---|---|
(?=…) |
Positive lookahead — what follows must match | yes | yes | yes |
(?!…) |
Negative lookahead — what follows must not | yes | yes | yes |
(?<=…) |
Positive lookbehind | since 2023 | fixed width | fixed width |
(?<!…) |
Negative lookbehind | since 2023 | fixed width | fixed width |
Lookaround is the one family where the summary in a table is actively misleading, because
all four forms exist in all three engines and still do not interchange. Lookbehind reached
every mainstream browser only in March 2023, and JavaScript is the odd engine out in allowing
a lookbehind of variable length: (?<=ab+)c runs fine there, while PCRE refuses to compile it
with lookbehind assertion is not fixed length and Python with look-behind requires
fixed-width pattern. The mechanism, and the reason capture groups inside a lookbehind come
back in the wrong order, are worked through in
lookahead and lookbehind.
Flags
JavaScript spells flags as letters after the closing slash. PCRE spells them as letters after
the closing delimiter too, and both PCRE and Python also accept them inline as (?i) at the
start of a pattern — which JavaScript rejects outright with Invalid group.
| Flag | Does | JS | PCRE | Py |
|---|---|---|---|---|
i |
Case-insensitive | yes | yes | re.I |
m |
^ and $ match at every line |
yes | yes | re.M |
s |
. also matches a line break |
yes | yes | re.S |
x |
Ignore whitespace, allow comments | — | yes | re.X |
g |
Find every match, not just the first | yes | n/a | n/a |
u |
Unicode mode and property escapes | yes | yes | default |
y |
Sticky — match only at lastIndex |
yes | — | — |
d |
Report start and end index per group | yes | — | default |
The g flag has no counterpart elsewhere because the question it answers is asked
differently: PHP has preg_match beside preg_match_all, and Python has re.search beside
re.findall. In JavaScript the choice is a letter on the pattern, and forgetting it is the
most common regex bug there is — which is why it gets a section of its own on
the replace page.
Replacement syntax
The replacement side is a separate small language, and it is the one part of regex where the three engines share almost no notation at all.
| Want | JS | PCRE / PHP | Python |
|---|---|---|---|
| Group 1 | $1 |
$1 or \1 |
\1 or \g<1> |
| A named group | $<name> |
number only | \g<name> |
| The whole match | $& |
$0 |
\g<0> |
| A literal dollar sign | $$ |
$ |
$ |
| Replace every match | /g or replaceAll |
default | default |
That last row is the one that bites. "a-b-c".replace(/-/, ":") returns a:b-c — one
replacement — while the equivalent call in PHP and Python replaces all three. The full set of
dollar tokens, the function form of a replacer, and the injection bug that follows from
letting user text into a replacement string are on
the regex replace page.
A regular expression cheat sheet for real jobs
Patterns you can paste. Every one was run before it went in, and the note beside it says what it does not promise.
| Job | Pattern | Note |
|---|---|---|
| Whole word | \bcat\b |
ASCII boundaries only, see above |
| Trailing whitespace | [ \t]+$ |
Add m to clean every line |
| Blank lines | ^[ \t]*$ |
With m. Using \s here lets one match span several lines |
| An ISO date | \d{4}-\d{2}-\d{2} |
Shape only, not validity |
| A hex color | #[0-9a-f]{3,8}\b |
Covers 3, 6 and 8 digit forms |
| A quoted string | "[^"\n]*" |
The \n stops runaway matching |
| Doubled word | \b(\w+)\s+\1\b |
A backreference earns its keep |
| Digits with separators | \d{1,3}(,\d{3})* |
Anchor it before trusting it |
| Everything but a word | ^(?!.*ERROR).*$ |
Negative lookahead, per line |
| An email address | see below | Read the next paragraph first |
There is no row for a correct email pattern because there is no correct email pattern. The shape can be checked and the address cannot, the RFC permits forms no validator accepts, and the specification browsers actually implement openly calls itself a deliberate violation of that RFC. What a pattern can honestly do, and what to use instead, is the whole of the email validation page. If your job is not to validate but to pull addresses out of a document you already have, the email extractor does that with the rejections shown rather than hidden.
The escapes that quietly turn into letters
This is the table that made the rest of this page worth writing. Every token here is an operator in PCRE and a plain letter in JavaScript. None of them raises an error. All of them change what your pattern matches.
| Written | In PCRE | In JavaScript |
|---|---|---|
\A |
Start of subject | The letter A |
\z \Z |
End of subject, strict and forgiving | The letters z and Z |
\h \H |
Horizontal whitespace, and not | The letters h and H |
\R |
Any line break, including CRLF | The letter R |
\K |
Reset the reported match start | The letter K |
\e |
The escape character | The letter e |
\Q…\E |
Treat the middle as literal text | The letters Q and E |
\R and \Q…\E are the two worth memorizing, because they appear in exactly the patterns
people copy: line-ending cleanup and escaping a variable into a pattern. Dropped into
JavaScript, the first silently stops matching carriage returns and the second stops escaping
anything at all.
Why grep is not a fourth column
grep -E is POSIX extended regular expressions, and POSIX has no lookaround at all. Run
grep -E 'a(?=b)' and you get repetition-operator operand invalid rather than a match,
because (?= is not a construct there and the ? is left with nothing to repeat. Shorthand classes are worse than absent: whether \d and \w
work at all depends on which grep your operating system shipped, and grep -P, which
switches the whole thing to PCRE, is missing entirely from the BSD grep that comes with
macOS. A column that says “sometimes, on some builds” is not a column. Use [[:digit:]],
which is genuinely portable, or reach for a tool with a named flavor.
Checking a token against the engine you will actually use
The reason this page carries a flavor column at all is that the alternative is finding out in
production. Any row above takes seconds to settle in the regex tester: it
runs the browser’s own engine rather than a stand-in for it, prints the position of each
match as well as its text, and calls out a construct that has quietly changed meaning on the
way in. Try \A\d{3}\z there against A300z and the anchor row above stops being trivia.
If the thing you are chasing turns out to be a difference between two files rather than a
pattern against one, switch instruments: the text comparison tool reports what
changed line by line, and it names line endings explicitly — which is the exact job \R was
supposed to do before it turned into the letter R.
- Regex email validation No pattern can tell you an address is real. What a regex honestly catches, what the HTML spec settles for, and why the RFC 5322 monster is not it.
- Regex lookahead and lookbehind A lookaround matches a position, not characters. The four forms, why they consume nothing, and the one your browser only got in March 2023.
- Regex replace The replacement side is its own language. What $1, $&, $$ and a replacer function do, and where a missing flag costs every match but the first.