Explain a regular expression
Paste a pattern and read it back piece by piece, in words — every character accounted for, groups indented, lazy quantifiers named.
Regular expression explainer
Just the pattern — no surrounding slashes. Flags go below.
Want to see it run? The tester highlights every match against your own text, and find and replace shows what a substitution would produce.
Reading a pattern from the outside in
A regular expression is a sequence of things to match, read left to right, where each thing can carry a repetition count. Almost every pattern that looks impenetrable is just a long sequence of very simple pieces, and the difficulty is that the syntax compresses them into single characters with no spaces between.
The breakdown above pulls them apart. Anchors (^ and $) pin the
match to the start or end. Character classes ([a-z]) admit one character from a
set. Quantifiers (*, +, ?, {2,5})
repeat whatever came immediately before. Groups (( )) bracket a section so a
quantifier applies to all of it, and capture what it matched. That is most of the language.
The three that catch everyone
- The unescaped dot.
.matches any character except a newline, sofile.txtmatches "fileXtxt" as happily as the name you meant. A literal period is\.— and inside a character class,[.], where it loses its special meaning automatically. - Greedy by default.
".*"against a line with two quoted strings matches from the first quote to the last, swallowing everything between. Adding a question mark —".*?"— makes it stop at the first closing quote. The breakdown labels this explicitly wherever it appears. - Alternation binds loosely.
^cat|dog$does not mean "exactly cat or dog". It means "starts with cat, or ends with dog", because the|splits the entire pattern rather than just the words either side of it. Brackets fix it:^(cat|dog)$.
Groups, and why numbering breaks patterns
Capture groups are numbered by the position of their opening parenthesis, counting from the left. That is fine until someone adds a group in the middle of an existing pattern, at which point every group after it shifts by one and any code referring to group 2 silently starts reading group 3. It is one of the more annoying ways a working regex breaks during a seemingly harmless edit.
Two things prevent it. Use (?:...) when you only need the brackets for grouping
and do not care what they matched — it takes no number at all. Use
(?<name>...) when you do need the value, and refer to it by name
thereafter. Note that a named group still consumes a number, which the breakdown above shows,
so mixing the two styles does not escape the problem entirely.
Lookaround: matching without consuming
A lookahead like foo(?=bar) matches "foo" only when "bar" comes next, but does
not include "bar" in the result — the match is three characters, not six. The negative form,
(?!...), requires that the following text is not there, which is the
standard way to express "any word except these". Lookbehind, (?<=...) and
(?<!...), does the same thing backwards.
This is the part of the syntax most worth learning deliberately, because there is no other way to say "the thing before or after must look like this, but I do not want it in my result", and rewriting a pattern to avoid lookaround usually means post-processing the match in code instead.
Common questions
What does this regex mean?
Paste it above and each piece is broken out on its own line with a description in plain English, indented to show what sits inside which group. The breakdown is exhaustive: every character of the pattern appears in exactly one row, so nothing is quietly skipped over. Reading down the list tells you what the pattern matches, in order, which is usually enough to understand an unfamiliar expression without needing to recall the syntax rules that produced it.
What is the difference between * and +?
Both repeat whatever comes immediately before them, but * allows zero repetitions and + requires at least one. That distinction matters more than it sounds: a* matches successfully against a string containing no 'a' at all, because zero is a valid count, whereas a+ does not. This is behind a large share of patterns that appear to match everything — a pattern built entirely from * quantifiers can match an empty string, and therefore matches at every position in the text.
What does a question mark after a quantifier do?
It makes the quantifier lazy, meaning it takes as few characters as it can rather than as many. The difference is easiest to see with .*? versus .* between two quotes: against a line holding two quoted strings, the greedy version swallows both quotes and everything between them, while the lazy version stops at the first closing quote. Greedy is the default and is usually what you want; lazy is what you want when the thing you are matching has a clear terminator. The breakdown labels every lazy quantifier explicitly, because it is a single character that entirely changes what a pattern captures.
What is the difference between a group and a non-capturing group?
Both bracket a section of the pattern so a quantifier or alternation applies to the whole of it. A plain ( ) additionally captures what it matched, making it available as a numbered result, while (?: ) does not. Use the non-capturing form when you only need the grouping, because every capturing group you add shifts the numbering of the ones after it — which is how a working pattern breaks when someone inserts a group in the middle. Named groups, written (?<name>...), avoid that problem entirely by giving the group a label instead of a position.
Why does my pattern match more than I expected?
Three causes account for most of it. An unescaped dot matches any character, not a literal period, so a pattern meant for a filename or an IP address matches far more than intended. A greedy quantifier extends to the last possible match on the line rather than the first. And an alternation without brackets binds looser than everything else — ^cat|dog$ means "starts with cat, or ends with dog", not "is exactly cat or dog", which needs ^(cat|dog)$. All three are visible in the breakdown above.
Does this cover other regex flavours?
The breakdown describes JavaScript's flavour, which is what runs in your browser and what this site's tester uses. Most of it transfers directly to Python, Java, Go, PCRE, and the others — quantifiers, character classes, groups, and anchors are essentially universal. The differences that bite are at the edges: lookbehind support varies by language and version, named group syntax differs, and \d means something slightly different once Unicode is involved. For everyday patterns, an explanation here is accurate everywhere.