Regular Expressions Cheatsheet

Common Patterns

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

Email, and the honest caveat

No regex fully validates an email address. RFC 5322 allows quoted local parts, comments, and bracketed IP literals, and even a perfect grammar cannot tell you the mailbox exists. The only real validation is sending a confirmation link.

[^@\s]+@[^@\s]+\.[^@\s]{2,}

That is the pragmatic version. It rejects the obvious typos and accepts everything a real user will type.

InputAccepted
me+tag@sub.example.com✓
x_y@z.io✓
a@b✗, no dot in the domain
a b@c.com✗, space
@b.co✗, empty local part

In the browser, <input type="email"> already applies the WHATWG grammar. Use it and skip the regex. Server side, check the shape, then send the mail and let the bounce be the answer.

IPv4 with real octet bounds

\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} accepts 999.999.999.999. Bound each octet instead.

(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)
InputResult
0.0.0.0, 192.168.1.1, 255.255.255.255✓
256.1.1.1✗, octet out of range
01.2.3.4✗, leading zero rejected
1.2.3✗, too few octets

Anchor it with ^…$ for validation, leave it unanchored to extract addresses from a log. For IPv6, do not write the regex, use ipaddress.ip_address() in Python, net.ParseIP in Go, or the platform equivalent.

URL, date, semver, and friends

# URL with parts, http and https only
(?P<scheme>https?)://(?P<host>[^/?#:\s]+)(?::(?P<port>\d+))?
(?P<path>/[^?#\s]*)?(?:\?(?P<query>[^#\s]*))?(?:#(?P<frag>\S*))?

# ISO 8601 calendar date, ranges bounded
\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])

# 24-hour time, optional seconds
(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?

# semver 2.0.0, the official pattern
(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)
(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?
(?:\+(?P<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?

# hex color, 3 4 6 or 8 digits
#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})

# UUID, any version 1-8
[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}

# URL slug
[a-z0-9]+(?:-[a-z0-9]+)*

The date pattern bounds the fields but not the calendar. It happily accepts 2026-02-31, so parse with the date library afterwards. Split the URL and semver patterns across lines only under the x flag, otherwise join them.

Capture a quoted string

"([^"]*)"                 simple, no escapes allowed inside
"((?:[^"\\]|\\.)*)"       correct, allows \" and \\ inside
(["'])((?:[^\\]|\\.)*?)\1 either quote style, ends with the same one
import re
re.findall(r'"((?:[^"\\]|\\.)*)"', r'say "hi \"there\"" and "x"')
# ['hi \\"there\\"', 'x']

The middle form is the one to memorize. It reads as "any char that is not a quote or a backslash, or a backslash followed by anything", repeated. It never backtracks catastrophically, because the two branches cannot both match the same character.

Trim, squeeze, and split

re.sub(r'^\s+|\s+$', '', s)            # trim both ends
re.sub(r'^[ \t]+|[ \t]+$', '', s)      # trim spaces and tabs, keep newlines
re.sub(r'\s+', ' ', s).strip()         # squeeze runs of whitespace
re.split(r'\s*,\s*', 'a , b,c')        # ['a', 'b', 'c']
re.split(r'\r\n|\r|\n', text)          # split lines, any ending

Every language ships a strip/trim that is faster and clearer than a regex. Use the regex only when you are trimming inside a larger substitution.

# squeeze whitespace, one line at a time
sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//' file

Parse a log line

Named groups turn a positional match into a dict, which is worth the extra characters.

COMBINED = re.compile(
    r'^(?P<ip>\S+) \S+ (?P<user>\S+) \[(?P<ts>[^\]]+)\] '
    r'"(?P<method>[A-Z]+) (?P<path>\S+) (?P<proto>[^"]+)" '
    r'(?P<status>\d{3}) (?P<bytes>\d+|-)')

m = COMBINED.match(line)
if m:
    d = m.groupdict()
    # {'ip': '1.2.3.4', 'method': 'GET', 'status': '200', ...}

[^\]]+ inside the brackets and [^"]+ inside the quotes are the important bits. A greedy .+ there would run to the last bracket or quote on the line.

CSV, and why you should not

A regex can tokenize a well-behaved CSV line, and it cannot parse CSV.

"([^"]*)"|([^,]+)          tokenizes a simple line, breaks on "" escapes

It fails on all of these, which are legal CSV:

  • an embedded doubled quote, "say ""hi"""
  • an embedded newline inside a quoted field, so the record spans lines
  • a quoted field containing a comma next to an unquoted one
  • a trailing empty field, a,b,
  • a BOM, \r\n endings, or a different delimiter

Use the real parser every time. csv in Python, encoding/csv in Go, Papa.parse in the browser, csvkit or mlr on the command line. The same rule holds for HTML, JSON, and YAML. If the format has a grammar, a regex is the wrong tool and the failures are silent.