RelicRELIC
Developer cheat sheets

Regex cheat sheet

Nobody keeps every regex token in their head. This cheat sheet lays out the pieces you actually reach for, character classes through lookarounds, with the flavor differences that bite you and a handful of patterns worth saving.

Jordan Gibbs July 10, 2026 9 min read

Regular expressions are a tiny language for describing text patterns, and like any language you use it in bursts and then forget half of it. The syntax below is grouped the way you tend to need it: the classes that match kinds of character, the anchors that pin a match to a position, the quantifiers that control repetition, then groups, lookarounds and flags. Everything is written for the common engines you meet day to day, and where they disagree, the disagreement is called out rather than glossed over.

The examples use JavaScript regex syntax unless noted, because it is the flavor in your browser, your regex tester, and most of the config files you touch. When Python or PCRE (the engine behind PHP, and close to what grep -P and many others use) differs in a way that matters, there is a note.

Character classes

A character class matches one character out of a set. Square brackets build your own set, and the backslash shorthands cover the common ones.

character classes
.        any character except a line break (unless dotall is on)
[abc]    one of a, b or c
[^abc]   any character that is NOT a, b or c
[a-z]    any lowercase letter (a range)
[a-zA-Z0-9_]   a word-ish set, spelled out
\d       a digit, same as [0-9]
\D       a non-digit
\w       a word character: letter, digit or underscore
\W       a non-word character
\s       any whitespace: space, tab, newline and friends
\S       any non-whitespace

Inside a class most metacharacters go quiet, so [.] is just a dot and [+*?] is those three literal symbols. The hyphen is the exception you have to think about: it builds a range in the middle, so to match a literal hyphen put it first or last, as in [-a-z] or [a-z-].

A flavor note on \\w and \\d: in JavaScript without the unicode flag, and in PCRE by default, these match ASCII only. Python 3 makes \\w and \\d Unicode-aware by default, so \\d there also matches digits from other scripts. Add the ASCII flag in Python, or the unicode flag in JavaScript, when you need to be explicit.

Anchors and boundaries

Anchors match a position rather than a character. They consume nothing; they assert that you are at a certain spot.

anchors and boundaries
^      start of the string (or line, with the multiline flag)
$      end of the string (or line, with the multiline flag)
\b     a word boundary: the edge between \w and \W
\B     a non-boundary: anywhere that is not a word edge
\A     start of the string, always (PCRE and Python; not JS)
\z     end of the string, always (PCRE and Python; not JS)

The word boundary \\b is the one people underuse. It is why \\bcat\\b matches the word cat but not the cat inside category or scatter. Note that ^ and $ change meaning under the multiline flag: without it they anchor to the whole string, with it they anchor to each line.

Quantifiers: greedy versus lazy

Quantifiers say how many times the thing before them may repeat. Each one has a greedy default and a lazy variant formed by adding a question mark.

quantifiers
*        zero or more (greedy)
+        one or more (greedy)
?        zero or one (optional)
{3}      exactly three
{2,5}    between two and five
{2,}     two or more
*?  +?  ??  {2,5}?     the lazy versions of each

Greedy is the default and it matches as much as possible before backing off. Lazy matches as little as possible and only grows when the rest of the pattern demands it. The classic trap is matching between delimiters: on the string below, greedy runs to the last bracket while lazy stops at the first.

greedy vs lazy on <a><b>
<.*>     matches <a><b>   (grabs everything, backs off to the final >)
<.*?>    matches <a>       (stops at the first > it can)

When a lazy quantifier still feels fiddly, matching a negated class is often clearer and faster: <[^>]*>reads as “a less-than, then anything that is not a greater-than, then a greater-than,” which does the same job without backtracking.

Groups and backreferences

Parentheses group part of a pattern so a quantifier can apply to the whole group, and they capture what they match so you can reuse it, in a replacement or later in the pattern.

groups
(abc)        a capturing group, referred to later as \1 or $1
(?:abc)      a non-capturing group: groups without capturing
(?<year>\d{4})   a named group (JS, PCRE, Python .NET-style)
a|b          alternation: match a or b
(cat|dog)s   groups the alternation, matches cats or dogs

A backreference matches the same text a group captured earlier. Inside the pattern you write \\1 for the first group; in a replacement string most tools use $1. So (\w)\1 finds a doubled letter, matching the ll in hello.

Named group syntax splits by flavor. JavaScript, PCRE and .NET write the definition as (?<name>...) and reference it in the pattern as \k<name>. Python instead uses (?P<name>...) to define and (?P=name) to reference. If a pattern fails only in Python, mismatched named-group syntax is a prime suspect.

Lookarounds

A lookaround checks whether something does or does not appear next to your match, without including it in the match. Look ahead of the current position, or behind it, and assert either presence or absence.

lookarounds
(?=foo)     positive lookahead: followed by foo
(?!foo)     negative lookahead: NOT followed by foo
(?<=foo)    positive lookbehind: preceded by foo
(?<!foo)    negative lookbehind: NOT preceded by foo

Lookaheads let you require a condition without consuming characters. A common use is a password-style check: ^(?=.*\d)(?=.*[A-Z]).{8,}$asserts “somewhere there is a digit” and “somewhere there is an uppercase letter” before matching eight or more of anything. Lookbehind is handy for grabbing a value after a label, like (?<=\$)\d+ to pull the number after a dollar sign without capturing the sign.

The portability caveat lives here. Lookahead is universal. Lookbehind is newer: it landed in JavaScript with ES2018 and works in current Node and browsers, and PCRE and Python support it too, but some older or lighter engines do not. Python additionally requires its lookbehind to be fixed-length, so (?<=ab) is fine while(?<=a+) is a syntax error there; JavaScript and PCRE are more relaxed about that.

Flags

Flags change how the whole pattern is applied. In JavaScript they hang off the end of a literal, as in /pattern/gi; in Python they are passed to the function or written inline as (?i) at the start.

flags
g   global: find every match, not just the first (JS)
i   case-insensitive
m   multiline: ^ and $ match at each line's edges
s   dotall: the dot also matches newlines
u   unicode mode (JS); needed for \p{...} and astral chars
x   extended: ignore whitespace and allow # comments (PCRE, Python)

Two of these sound related but do separate jobs. The multiline flag (m) only changes what ^ and $ mean; it does nothing to the dot. The dotall flag (s) only changes the dot; it does nothing to the anchors. To make the dot cross newlines, reach for s; m is the one that moves the anchors. The extended flag (x), available in PCRE and Python but not plain JavaScript, lets you spread a gnarly pattern over several lines with comments, which is worth its weight on anything you will reread.

Patterns you actually reuse

Cheat sheets earn their keep on the patterns you would otherwise rewrite. These are safe, self-contained ones. Test any of them live in the regex tester, which runs the browser’s own JavaScript engine, so what matches there matches in your code.

Trim leading and trailing whitespace

Match the whitespace at the very start or the very end of a line and replace it with nothing. Add the multiline flag so it applies at every line’s edges rather than only the ends of the whole string.

trim whitespace (replace with empty, flags: g m)
^\s+|\s+$

Extract every run of digits

Pull each number out of mixed text. With the global flag this yields one match per number; each match is a full run of digits rather than single characters.

digits (flags: g)
\d+

Match a whole line

Under the multiline flag, this captures the content of each line without the line breaks. The * allows empty lines to match too.

each line's content (flags: g m)
^.*$

Match an ISO date

A shape check for a YYYY-MM-DD date, anchored so it matches the whole field. This validates the format, not the calendar: it will accept 2026-13-40, so treat it as a first pass and parse the result to confirm the day is real.

ISO date shape
^\d{4}-\d{2}-\d{2}$

A word on that last caveat, because it applies broadly: a regex is good at shape and bad at meaning. It can tell an email-shaped string from a random one, but it cannot tell you the address exists, and the fully correct email regex is famously enormous. For validation, match a sensible shape and let a real parser or a live check do the rest.

Where flavors diverge, briefly

  • Lookbehind: everywhere modern except some lightweight engines; Python needs it fixed-length.
  • Named groups: (?<name>...) in JS, PCRE and .NET; (?P<name>...) in Python.
  • \\A and \\z: real string anchors in PCRE and Python; JavaScript has only ^ and $.
  • Unicode \\w and \\d: ASCII by default in JS and PCRE, Unicode by default in Python 3.
  • The x flag: PCRE and Python let you comment and space out patterns; plain JavaScript does not.

For the formats you paste around a shell all day, a clean copy matters as much as a correct pattern; our guide to copy and paste in the terminal covers the shortcuts that will not mangle it. And if you keep coming back to jq or subnet math, the sibling jq cheat sheet and subnet cheat sheet follow the same format.

Frequently asked questions

Do I need to escape a dot inside a character class?

No. Inside square brackets, a dot is a literal period, so [.] and \. both match a single dot. Most metacharacters lose their special meaning inside a class. The characters that still need care in there are ^ (only special as the first character, where it negates), the closing ], the backslash, and the hyphen, which forms a range unless it sits first or last.

What is the difference between greedy and lazy quantifiers?

A greedy quantifier like .* grabs as much text as it can and then gives back only what it must to let the rest of the pattern match. A lazy quantifier like .*? grabs as little as possible and expands only when forced. On the string <a><b>, the pattern <.*> matches the whole thing, while <.*?> matches just <a>. Reach for lazy when you are matching between delimiters.

Does JavaScript support lookbehind?

Modern JavaScript does. Lookbehind, both positive (?<=...) and negative (?<!...), has been part of the language since ES2018 and works in current Node and every current browser. Older runtimes and some other engines lack it, which is the main portability gotcha. Lookahead, by contrast, has worked everywhere for a long time.

Why does my pattern match too much across newlines?

By default the dot matches any character except a line break, and the ^ and $ anchors bind to the whole string. If you want the dot to cross newlines, add the dotall flag (s). If you want ^ and $ to match at the start and end of every line, add the multiline flag (m). They are separate switches and are easy to confuse.

Written by
Jordan GibbsFounder, Relic

Jordan Gibbs is the founder of Relic, an end-to-end encrypted, permanent, searchable memory for everything you copy. He writes widely about AI, agents, and practical tooling on Medium, where he is read by tens of thousands, and builds privacy-first software. Here he covers how everyday tools like the clipboard actually work, and how to use them without handing your data to someone else.

MediumGitHubLinkedIn
Part of
Developer cheat sheets
Keep reading
8 min

JSONPath examples

JSONPath is XPath for JSON. One small store document, threaded through wildcards, slices, filters and recursive descent, with the honest note on how implementations vary.

Read
8 min

jq cheat sheet

jq is sed for JSON. The filters you use most, built up against one API response so every command has a visible result, from map and select to group_by and raw output.

Read
8 min

wget vs curl: what is the difference?

wget and curl both fetch over HTTP, and there the resemblance ends. One is a recursive downloader, the other a protocol swiss army knife. What each is genuinely better at.

Read
developerregexreference