creatorvalet

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.

Updated 2026-08-09

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.

More on this

  • 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.