By the DevDash Team · Last updated July 2026
Regex Guide: A Complete Regular Expressions Tutorial & Cheat Sheet
Regular expressions are one of those skills that separate developers who spend minutes on text problems from developers who spend hours. Whether you're validating user input, parsing log files, extracting data from messy strings, or building search features, regex gives you a concise language to describe exactly the pattern you need. This guide covers everything from basic syntax to advanced techniques, with practical patterns you can copy and use immediately.
What Are Regular Expressions and Why Every Developer Needs Them
A regular expression — usually shortened to "regex" or "regexp" — is a sequence of characters that defines a search pattern. You write a pattern, and the regex engine scans through text to find matches. That pattern can be as simple as a literal word or as complex as a rule describing every valid email address.
Regex is not tied to any single language. JavaScript, Python, Java, Go, Ruby, PHP, and command-line tools like grep and sed all support regular expressions with only minor syntax differences. Learning regex once gives you a tool that works everywhere.
You'll reach for regex when you need to validate form inputs (does this look like an email?), extract structured data from unstructured text (pull all URLs from a page), transform strings in bulk (rename every file matching a pattern), or build search features that go beyond simple substring matching. The alternative — writing nested loops and conditionals to do the same thing — is almost always more code, more bugs, and more time.
Basic Syntax: The Building Blocks
Literals
The simplest regex is a literal string. The pattern hello matches the exact sequence "hello" anywhere in the input. No special syntax, no wildcards — just a direct match. Most regex patterns combine literals with special characters called metacharacters.
Character classes
A character class matches any one character from a set. Square brackets define the set: [aeiou] matches any single vowel. You can use ranges — [a-z] matches any lowercase letter, [0-9] matches any digit. Negate a class with a caret: [^0-9] matches anything that is not a digit.
Shorthand classes save typing: \d for digits, \w for word characters (letters, digits, underscore), and \s for whitespace. Their uppercase versions (\D, \W, \S) match the opposite.
Quantifiers
Quantifiers control how many times a token must appear. * means zero or more, + means one or more, and ? means zero or one (making the previous token optional). For exact counts, use curly braces: {3} matches exactly three, {2,5} matches between two and five, and {3,} matches three or more.
Anchors
Anchors don't match characters — they match positions. ^ matches the start of a line, $ matches the end. \b matches a word boundary — the position between a word character and a non-word character. Anchors are essential for validation: without ^ and $, a pattern might match a substring when you intended to validate the entire input.
Regex Cheat Sheet: Common Tokens at a Glance
| Token | Description | Example |
|---|---|---|
| . | Matches any single character except newline (unless s flag is set) | a.c matches "abc", "a1c", "a-c" |
| \d | Matches any digit (0-9) | \d{3} matches "123", "456" |
| \w | Matches any word character (letter, digit, underscore) | \w+ matches "hello_42" |
| \s | Matches any whitespace character (space, tab, newline) | a\sb matches "a b", "a\tb" |
| \D, \W, \S | Negated versions — match non-digit, non-word, non-whitespace | \D+ matches "abc", "--" |
| * | Matches zero or more of the preceding token (greedy) | ab*c matches "ac", "abc", "abbc" |
| + | Matches one or more of the preceding token (greedy) | ab+c matches "abc", "abbc" but not "ac" |
| ? | Matches zero or one of the preceding token (optional) | colou?r matches "color" and "colour" |
| {n,m} | Matches between n and m of the preceding token | a{2,4} matches "aa", "aaa", "aaaa" |
| ^ | Anchors match to the start of a line | ^Hello matches "Hello world" |
| $ | Anchors match to the end of a line | world$ matches "Hello world" |
| \b | Word boundary — between a word character and a non-word character | \bcat\b matches "cat" but not "catch" |
| [abc] | Character class — matches any one of the enclosed characters | [aeiou] matches any vowel |
| [^abc] | Negated class — matches any character not listed | [^0-9] matches any non-digit |
| (abc) | Capture group — groups tokens and captures the match | (\d+)-(\d+) captures both numbers |
| (?:abc) | Non-capturing group — groups without capturing | (?:ab)+ matches "ababab" |
| a|b | Alternation — matches either the left or right side | cat|dog matches "cat" or "dog" |
| \1 | Backreference — refers to the first capture group | (\w+)\s\1 matches "the the" |
Capture Groups and Backreferences
Parentheses do double duty in regex: they group tokens together so you can apply quantifiers to the group, and they capture the matched text so you can reference it later. When you write (\d{4})-(\d{2})-(\d{2}), the engine matches a date like "2026-07-13" and stores each part in a numbered capture group: group 1 is "2026", group 2 is "07", group 3 is "13".
In replacement strings, you reference captures with $1, $2, etc. To reformat that date to "13/07/2026", your replacement is $3/$2/$1.
// JavaScript example
const dateStr = "2026-07-13";
const reformatted = dateStr.replace(
/(\d{4})-(\d{2})-(\d{2})/,
"$3/$2/$1"
);
// Result: "13/07/2026"Backreferences let you match the same text again within the pattern itself. (\w+)\s\1 matches repeated words like "the the" — group 1 captures "the", and \1 requires the exact same text to appear again.
Named capture groups
Numbered groups get confusing in complex patterns. Named groups solve this. The syntax varies slightly by language, but in JavaScript and Python you write (?<year>\d{4}). You can then reference the match by name instead of number, making your code far more readable.
// Named capture groups in JavaScript
const match = "2026-07-13".match(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
);
console.log(match.groups.year); // "2026"
console.log(match.groups.month); // "07"
console.log(match.groups.day); // "13"If you need grouping but don't need to capture, use a non-capturing group: (?:abc). This keeps your capture group numbering clean and is marginally faster.
Lookaheads and Lookbehinds
Lookarounds are zero-width assertions: they check whether a pattern exists ahead of or behind the current position, but they don't consume any characters. The matched text itself doesn't include whatever the lookaround checked. There are four types:
(?=...) Positive lookahead — what follows must match (?!...) Negative lookahead — what follows must NOT match (?<=...) Positive lookbehind — what precedes must match (?<!...) Negative lookbehind — what precedes must NOT match
When you actually need them
Lookarounds shine when you need context-dependent matching. Suppose you want to match prices but only when they're in dollars — not euros or pounds. A lookbehind does this cleanly:
(?<=\$)\d+\.\d{2}
// Matches "9.99" in "$9.99" but not in "€9.99"Negative lookaheads are common in password validation. To require that a string contains at least one digit, one lowercase letter, and one uppercase letter:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
// At least 8 chars, with lowercase, uppercase, and a digitA practical use of negative lookaheads: matching "foo" only when it's not followed by "bar" — foo(?!bar). This matches "football" and "foo" but not "foobar".
Keep in mind that lookbehinds have limitations in some engines. JavaScript only added lookbehind support in ES2018, and some engines (notably older versions of Safari) may not support variable-length lookbehinds.
Common Regex Patterns Developers Use Daily
These are battle-tested patterns for common validation and extraction tasks. Note that some of these (particularly email) are simplified — perfect validation for edge cases often requires additional logic beyond regex.
Email validation
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$Covers the vast majority of real-world email addresses. The RFC 5322 spec allows far more exotic formats, but this pattern strikes the right balance between strictness and practicality.
URL matching
https?:\/\/[\w.-]+(?:\.[a-zA-Z]{2,})(?:[\/\w._~:?#\[\]@!$&'()*+,;=-]*)*Matches HTTP and HTTPS URLs with paths and query parameters. For strict URL validation, consider using the built-in URL constructor in JavaScript and catching the error.
IPv4 address
^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$Validates that each octet is between 0 and 255. A naive \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} pattern would accept invalid addresses like "999.999.999.999".
Date (YYYY-MM-DD)
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$Validates ISO 8601 date format with reasonable month and day ranges. Note that regex alone can't validate dates fully (February 30 would pass) — use date parsing libraries for that.
Phone number (US format)
^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$Handles formats like "(555) 123-4567", "555-123-4567", "+1 555 123 4567", and "5551234567". For international numbers, consider a library like libphonenumber.
Semantic version (SemVer)
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
The official SemVer regex from semver.org. Matches versions like "1.0.0", "2.1.0-beta.1", and "3.0.0-rc.1+build.123". For simple major.minor.patch validation, ^\d+\.\d+\.\d+$ is often enough.
Regex Flags Explained
Flags modify how the regex engine interprets your pattern. You set them after the closing delimiter in languages like JavaScript (/pattern/gi) or as function arguments in Python (re.IGNORECASE).
g — Global
Without the gflag, the engine stops after the first match. With it, the engine finds every match in the string. Essential when you're extracting all occurrences or doing a global find-and-replace.
i — Case-insensitive
Makes the pattern match regardless of letter casing. /hello/i matches "Hello", "HELLO", and "hElLo".
m — Multiline
Changes the behavior of ^ and $. Without this flag, they match the start and end of the entire string. With m, they match the start and end of each line. Critical when working with multi-line text like log files.
s — DotAll
By default, the dot (.) matches any character except newlines. The s flag makes it match newlines too. Useful when you need to match text that spans multiple lines, like extracting the contents of an HTML tag that contains line breaks.
u — Unicode
Enables full Unicode matching. Without it, JavaScript treats surrogate pairs as two separate characters, which means patterns like . might not match emoji or characters outside the Basic Multilingual Plane correctly. If your input might contain non-ASCII characters, always use the u flag.
Common Mistakes and How to Avoid Them
Catastrophic backtracking
This is the most dangerous regex pitfall. It happens when a pattern has nested quantifiers that create an exponential number of possible paths for the engine to try. A pattern like (a+)+b looks harmless, but when applied to a string of a's with no trailing "b", the engine tries every possible way to divide those a's among the groups before giving up. With 25 characters, that's over 33 million combinations.
Prevention: avoid nested quantifiers, use atomic groups or possessive quantifiers where your engine supports them, and always test patterns against inputs designed to not match.
Greedy vs. lazy — using the wrong one
By default, quantifiers are greedy: they match as much text as possible. This frequently surprises developers working with HTML or delimited text. Consider matching the content of an HTML bold tag:
// Input: "<b>hello</b> and <b>world</b>" // Greedy: <b>.+</b> → matches "<b>hello</b> and <b>world</b>" // Lazy: <b>.+?</b> → matches "<b>hello</b>" (first match)
Adding ? after a quantifier makes it lazy — it matches as little text as possible. Use lazy quantifiers when you need the shortest match.
Forgetting to escape special characters
Characters like ., *, +, ?, (, ), [, {, ^, $, |, and \ have special meaning in regex. To match them literally, escape with a backslash. Want to match an actual dot? Use \., not .. This is the most common source of subtle bugs in regex patterns.
Not anchoring validation patterns
If you're using regex to validate an entire input (like an email field), always anchor your pattern with ^ and $. Without anchors, the pattern \d{3}-\d{4} would match inside "call 555-1234 now", even though the full string is not a phone number.
Testing Regex Interactively vs. Writing Them Blind
Writing regex without testing it interactively is like writing SQL without running the query. You might get lucky, but you'll probably spend more time debugging than you saved. The difference is significant: when you type a pattern into a live regex tester, you see matches highlight in real time, capture groups appear instantly, and you can iterate on the pattern until it does exactly what you need.
Compare that to the alternative: write the regex in your code, run the program, check the output, realize it matched too much, go back to the code, adjust a quantifier, run again, check again. Each cycle takes a minute. In a regex tester, each adjustment takes a second.
The best regex testers also help you learn. When you see capture groups highlighted in different colors, or when hover text explains what each part of your pattern does, you build intuition much faster than reading documentation alone. DevDash's Regex Tester provides real-time match highlighting and flag controls, making it the fastest way to build and verify patterns before dropping them into your codebase.
Regex in JavaScript, Python, and the Command Line
Regex syntax is mostly consistent across languages, but there are meaningful differences in how you create, apply, and work with patterns. Here are the practical differences you need to know.
JavaScript
JavaScript uses regex literals with forward slashes or the RegExp constructor. Key methods are .test() (returns boolean), .match() (returns matches), and .replace() (substitution).
// Regex literal
const pattern = /\d{3}-\d{4}/g;
const found = "call 555-1234 or 555-5678".match(pattern);
// ["555-1234", "555-5678"]
// RegExp constructor (useful for dynamic patterns)
const dynamic = new RegExp("\\d{3}-\\d{4}", "g");
// matchAll for capture groups (ES2020+)
const datePattern = /(?<y>\d{4})-(?<m>\d{2})/g;
for (const m of "2025-01, 2026-07".matchAll(datePattern)) {
console.log(m.groups.y, m.groups.m);
}Python
Python's re module uses raw strings (r"...") to avoid double-escaping backslashes. Key functions are re.search(), re.findall(), re.sub(), and re.compile().
import re
# Find all matches
emails = re.findall(r'[\w.+-]+@[\w-]+\.[\w.]+', text)
# Named groups
match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', "v3.14")
if match:
print(match.group("major")) # "3"
# Compile for reuse (faster in loops)
pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')
if pattern.match("2026-07-13"):
print("Valid date format")Command line (grep and sed)
On the command line, grep searches for patterns and sed performs substitutions. The main gotcha: basic regex in grep requires escaping parentheses and quantifiers, while grep -E (extended regex) and grep -P (Perl-compatible) use the syntax you're used to.
# Find all lines containing an IP address
grep -E '\b[0-9]{1,3}(\.[0-9]{1,3}){3}\b' access.log
# Extract email addresses from a file
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' contacts.txt
# Replace dates from YYYY-MM-DD to DD/MM/YYYY
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/g' data.csv
# Find files containing TODO comments
grep -rn 'TODO:.*' src/Key differences to remember
JavaScript uses /pattern/flags syntax; Python uses r"pattern" with flag constants. Python names groups with (?P<name>...) while JavaScript uses (?<name>...). In sed, backreferences in replacement use \1 while JavaScript uses $1. Despite these differences, the core pattern syntax is nearly identical — which is why learning regex pays off across your entire stack.
Reading about regex builds understanding, but real fluency comes from practice. The fastest way to internalize these patterns is to type them into a live tester, see what matches, adjust, and repeat. DevDash's Regex Tester runs entirely in your browser — paste your text, write your pattern, and see matches highlighted in real time. No accounts, no installs, no data sent to any server.