ToolKitSphere IconToolKitSphere
Developer Utilities

The Complete Guide to Regular Expressions

Online Tools Platform Team13 min read

Regular expressions are a compact language for describing patterns in text. Instead of writing a loop that walks characters one at a time, you write a single pattern that says "a sequence of digits, then a dash, then three letters" — and the regex engine does the scanning, matching, and extracting for you. Every mainstream language ships an engine: JavaScript, Python, Java, Go, Rust, C#, PHP, Ruby, plus the command-line tools you already use like grep, sed, and ripgrep. That ubiquity is why regex earns a permanent place in a developer's toolkit. You will reach for it when validating user input, parsing log files, renaming a thousand files at once, extracting fields from a scraped page, refactoring code across a repository, or splitting messy CSV-ish data that no proper parser will touch. The syntax has a reputation for being cryptic, and honestly it deserves some of it — but the reputation comes from density, not complexity. There are roughly a dozen concepts underneath the whole thing. This regex guide walks through all of them in order, from literal characters to catastrophic backtracking, with working examples you can paste straight into a tester. By the end you will be able to read an unfamiliar pattern, write your own, and recognize the handful of constructs that turn a fast pattern into a slow one. Treat it as both a tutorial and a regex cheat sheet you can come back to.

Regex Syntax Basics

Every pattern is built from two kinds of characters: literals, which match themselves, and metacharacters, which mean something special. The pattern cat matches the three letters c-a-t anywhere in the input. That is a complete, valid regex. Everything else is a way to say "not exactly this, but something like this."

The metacharacters are . ^ $ * + ? { } [ ] \ | ( ). To match one of them literally, escape it with a backslash: \. matches a period, \$ matches a dollar sign, \\ matches a single backslash.

The Dot and Character Classes

An unescaped . matches any single character except a newline. It is the bluntest tool in the language and usually not what you want — . in 3.14 will happily match 3x14 too.

Character classes narrow that down. Square brackets define a set, and the class matches exactly one character from it:

  • [abc] — one character: a, b, or c
  • [a-z] — one lowercase letter (ranges use ASCII/Unicode ordering)
  • [A-Za-z0-9_] — one word character
  • [^0-9] — one character that is not a digit (a leading ^ negates the class)

Inside a class the rules relax: . is a literal dot, and [.] is a perfectly readable alternative to \.. A hyphen is literal when it is first or last, so [-+] and [a-z-] both work as written.

Shorthand classes cover the common sets:

Shorthand Matches Negated form
\d a digit \D
\w word character: letter, digit, or underscore \W
\s whitespace: space, tab, newline, and friends \S

Anchors

Anchors match a position, not a character. ^ asserts the start of the string and $ asserts the end. The difference matters more than beginners expect: \d{3} finds three digits somewhere in abc123def, while ^\d{3}$ matches only a string that is exactly three digits and nothing else. Validation patterns should almost always be anchored at both ends — an unanchored validator will accept bob@example.com; DROP TABLE users because the pattern only needs to match a substring.

\b is a word boundary: the zero-width position between a \w and a non-\w. \bcat\b matches cat in "the cat sat" but not in "concatenate."

Quantifiers

Quantifiers say how many times the preceding element repeats:

  • * — zero or more
  • + — one or more
  • ? — zero or one (optional)
  • {3} — exactly three
  • {2,5} — two to five
  • {2,} — two or more

A quantifier applies to the single element immediately before it. ab+ matches abbb, not ababab — for that you need a group: (ab)+.

Groups and Alternation

Parentheses do two jobs. They group a subpattern so a quantifier or alternation applies to the whole thing, and they capture the matched text for later use.

The pipe | means "or," and it has the lowest precedence in the language. ^cat|dog$ does not mean what it looks like — it means "starts with cat, OR ends with dog." Wrap the alternation to scope it: ^(cat|dog)$.

A Practical Email Validation Pattern

Email is the canonical worked example, so let's do it properly. The full RFC 5322 grammar permits quoted local parts, comments, and folded whitespace; no sane production regex covers it. What you actually want is a pattern that rejects obvious typos and accepts everything a real user will type:

^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$

Reading it left to right:

  • ^ — anchor to the start of the string.
  • [A-Za-z0-9._%+-]+ — the local part: one or more letters, digits, or the punctuation commonly allowed before the @. The + and % are literal inside a class; the - is last, so it is literal too.
  • @ — a literal at sign.
  • [A-Za-z0-9-]+ — the first domain label, such as mail in mail.example.co.uk.
  • (?:\.[A-Za-z0-9-]+)* — zero or more additional labels, each a dot followed by a label. This is a non-capturing group, so it does the grouping work without adding a capture you would never read.
  • \.[A-Za-z]{2,} — the final dot and the TLD: at least two letters.
  • $ — anchor to the end.

This accepts first.last+tag@sub.example.co.uk and rejects bob@, bob@@x.com, bob@example, and bob example.com. It will also accept some addresses that no mail server hosts, which is fine — the only authoritative validation is sending a message and seeing if it arrives. Paste it into the Regex Tester & Matcher with a list of real and malformed addresses to see exactly which side of the line each one falls on.

Flags Change How the Whole Pattern Behaves

Flags (also called modifiers) are set outside the pattern body and apply globally to it. The three you will use constantly:

g — global. Find every match instead of stopping at the first. Without g, a JavaScript replace swaps only the first occurrence. With it, all of them. In JavaScript, g also gives the regex object a mutable lastIndex, which is why reusing a single global regex across calls to .test() produces the infamous alternating true/false bug — create the regex fresh, or reset lastIndex.

i — case-insensitive. /error/i matches Error, ERROR, and eRrOr. Prefer this over writing [Ee][Rr][Rr][Oo][Rr].

m — multiline. Redefines ^ and $ to match at the start and end of each line rather than the whole string. This is the flag people reach for when processing log files line by line. Note that m does not affect . — for that you want the separate s (dotAll) flag, which lets . match newlines as well.

Most engines also offer u for full Unicode handling and x for whitespace-insensitive "extended" patterns that let you comment a complex regex across multiple lines. JavaScript has u/v but no x; Python, Java, and PCRE all support extended mode.

Capture Groups vs Non-Capturing Groups

Every (...) creates a numbered capture, counted by the position of its opening parenthesis, starting at 1. Given the pattern (\d{4})-(\d{2})-(\d{2}) against 2026-08-06, group 1 is 2026, group 2 is 08, group 3 is 06. Group 0 is always the entire match.

Captures are how you extract data, and how you reference matched text elsewhere:

  • In the pattern — a backreference. (\w+)\s+\1 matches a doubled word like the the, because \1 requires the same text the first group captured.
  • In a replacement string$1 (JavaScript, .NET, PHP) or \1 (Python, sed) inserts the captured text. Rewriting 2026-08-06 to 08/06/2026 is a one-liner: match (\d{4})-(\d{2})-(\d{2}), replace with $2/$3/$1.

Named groups make longer patterns readable: (?<year>\d{4})-(?<month>\d{2}) gives you match.groups.year instead of match[1]. Supported in JavaScript (ES2018+), Python, Java, .NET, and PCRE.

When you need the grouping but not the value, use a non-capturing group (?:...). It is slightly cheaper and, more importantly, it keeps your capture numbers stable — adding a (?:...) in the middle of a pattern will not silently shift $2 to $3 and break your replacement.

Two zero-width group types round out the set. Lookahead (?=...) and negative lookahead (?!...) assert that something does or does not follow, without consuming it. Lookbehind (?<=...) and (?<!...) do the same backwards. A classic use is a password rule: ^(?=.*[A-Z])(?=.*\d).{8,}$ requires at least one uppercase letter, at least one digit, and eight or more characters total — three independent conditions checked at the same starting position.

Greedy vs Lazy Quantifiers

By default, quantifiers are greedy: they consume as much as possible, then give characters back one at a time until the rest of the pattern can match. This trips up nearly everyone the first time.

Run <.+> against <b>bold</b>. You might expect <b>. You get <b>bold</b>, because .+ grabs the whole string, then backtracks just far enough to find a final > — which is the last one.

Add a ? to make the quantifier lazy: <.+?> matches as little as possible and returns <b>. Lazy variants exist for all of them: *?, +?, ??, {2,5}?.

There is a third option that is often better than either: use a negated character class. <[^>]+> says "a <, then one or more characters that are not >, then a >." It cannot overshoot, so there is nothing to backtrack, and it is measurably faster than the lazy version on long inputs. When you find yourself reaching for .*?, ask whether a negated class expresses the intent more precisely.

Performance and Catastrophic Backtracking

Most regex engines in wide use — JavaScript, Python, Java, .NET, PCRE, Ruby — are backtracking engines. They explore possible matches by trial and error, and for the overwhelming majority of patterns that is fast and completely fine. The failure mode is narrow but severe.

The trigger is a quantifier applied to something that is itself quantified and ambiguous. The textbook case is ^(a+)+$. Against a string of 30 a characters followed by a single b, the engine must try every way of partitioning those as among the repetitions of the group before it can conclude that no match exists. The work grows exponentially with input length: 25 characters may finish instantly, 35 may take minutes. That is catastrophic backtracking, and when an attacker controls the input it becomes a ReDoS denial-of-service vulnerability.

Real-world offenders look less obvious. (\s*\w+)*$ and ^(\w+\s?)*$ have the same shape. So does (a|a)*, where the alternatives overlap. The pattern to recognize is: nested quantifiers whose inner and outer parts can match the same text.

Practical defenses:

  • Make alternatives and adjacent classes mutually exclusive so only one parse is possible. (\s*\w+)* becomes safe when reworked so whitespace and word characters cannot both match the same position.
  • Anchor patterns and prefer negated classes over .* where you can.
  • Use atomic groups (?>...) or possessive quantifiers ++, *+ in engines that support them — Java, PCRE, .NET, Ruby. JavaScript supports neither, so you must fix the pattern itself.
  • Reach for a linear-time engine when input is untrusted. Go's regexp and Rust's regex crate use finite automata and guarantee linear time (giving up backreferences and lookaround in exchange). .NET 7+ offers RegexOptions.NonBacktracking, and .NET also supports a match timeout.
  • Cap input length before matching. A 200-character limit turns an exponential blowup into a non-event.

Beyond backtracking, the ordinary performance advice is simple: compile patterns once and reuse them rather than rebuilding them inside a loop, and put the most selective part of an alternation first so the engine fails fast.

Fitting Regex Into Your Workflow

Writing regex by guessing and re-running your application is the slow path. A tighter loop looks like this, and every tool in it runs entirely in your browser — the log lines, API payloads, and customer data you paste in never leave your machine, which matters when the text you are debugging contains anything you would not post publicly.

Draft and verify. Start in the Regex Tester & Matcher with a handful of strings that must match and, just as important, a handful that must not. Highlighted matches and captured groups tell you immediately whether + should have been * or whether your anchors are missing. Test the negative cases first; a pattern that accepts too much is the more common bug.

Understand what you inherited. When a pattern shows up in a code review or an old validation rule and nobody remembers what it does, run it through the Regex Explainer & Pattern Breakdown. It decomposes the pattern token by token, which is far faster than mentally parsing nested groups and also surfaces the nested-quantifier shapes described above.

Transform text. For bulk edits — reformatting dates, stripping HTML tags, renaming a symbol with context, converting snake_case to camelCase across a file — the Regex Find & Replace tool applies a pattern with $1-style backreferences and shows the result before you commit to it. Dry-running a replacement here beats discovering a bad substitution in your git diff.

Break apart messy input. When a delimiter is inconsistent — mixed commas and semicolons, runs of whitespace, or a separator that only appears between certain fields — the Regex Split Tool splits on a pattern rather than a fixed string. \s*[,;]\s* handles a list that has been hand-edited by three different people.

Conclusion

Regular expressions come down to a small set of building blocks: literals and character classes to say what, quantifiers to say how many, anchors to say where, groups to say which part matters, and flags to set the ground rules. Once those click, reading an unfamiliar pattern becomes mechanical. The judgment calls that remain are the interesting ones — anchoring validators so they cannot match a substring, choosing a negated class over a lazy quantifier, and spotting the nested quantifiers that turn a harmless pattern into a ReDoS risk.

The fastest way to internalize all of it is to write patterns against real data and watch what they match. Open the Regex Tester & Matcher, paste in the email pattern from this guide alongside a dozen addresses, and start breaking it. It is free, it needs no account, and everything you paste stays in your browser.

Frequently asked questions

What does \d mean in regex?

\d matches a single digit character. In JavaScript, Perl, and most PCRE-style engines it is equivalent to [0-9] by default. In .NET and in Python 3 with str patterns, \d also matches Unicode digits from other scripts unless you pass an ASCII-only option.

How do I match a literal dot in regex?

Escape it as \. or place it inside a character class as [.]. An unescaped dot is a metacharacter that matches any character except a newline, so example\.com matches only the literal text "example.com".

Is regex the same in every programming language?

The core syntax — literals, character classes, anchors, quantifiers, groups, and alternation — is nearly identical everywhere. Differences appear in lookbehind support, named group syntax, Unicode handling, and available flags, so always test a pattern in the engine you will actually ship it in.

What is the difference between greedy and lazy quantifiers?

Greedy quantifiers like * and + match as much text as possible and then backtrack until the rest of the pattern fits. Lazy quantifiers, written by adding ? (as in *? or +?), match as little as possible and expand only when forced.

What does the g flag do in regex?

The g (global) flag makes the engine find every match in the string instead of stopping at the first one. In JavaScript it is required for String.prototype.replaceAll with a regex and for iterating matches with matchAll.

What is a non-capturing group in regex?

A non-capturing group is written (?:...) and groups part of a pattern for quantifiers or alternation without storing the matched text as a numbered capture. Use it when you need the grouping but not the captured value, since it keeps capture numbering clean.

Can regex validate an email address perfectly?

No. The full RFC 5322 grammar allows quoted strings, comments, and nested constructs that a practical regex will not cover. Use a pragmatic pattern to catch obvious typos, then confirm the address by sending a verification email.

What is catastrophic backtracking?

Catastrophic backtracking happens when nested or overlapping quantifiers — such as (a+)+ — force a backtracking engine to try exponentially many ways to match a failing string. It can hang a request for seconds or minutes, and it is the root cause of most ReDoS vulnerabilities.

Try the related tools

Related articles