Regex to Match Text Between Two Strings (Brackets, Quotes, Tags)
How to match text between two strings in regex — the lazy .*? pattern, the negated-class version, multiline, and excluding the delimiters themselves.
The pattern is START(.*?)END — your opening delimiter, a lazy capture group, your closing delimiter. The .*? is the key: lazy matching stops at the first END instead of the last. Group 1 holds the text between them. Paste any example below into the regex tester to watch the match boundaries.
The recipe for each shape
| You want text between | Pattern | Group 1 on a(b)c / x"y"z |
|---|---|---|
| Parentheses | \(([^)]*)\) |
a(b)c → b |
| Double quotes | "([^"]*)" |
x"y"z → y |
| Square brackets | \[([^\]]*)\] |
a[b]c → b |
| Words/tags | START(.*?)END |
aSTARTmidENDz → mid |
| …across line breaks | /START(.*?)END/s |
s flag makes . match \n |
| …without the delimiters | (?<=START).*?(?=END) |
lookaround = match is the inside only |
Why .*? and not .*
Greedy vs lazy decides everything here. With <b>one</b> x <b>two</b>:
<b>(.*)</b>→ one match:one</b> x <b>two— greedy runs to the last</b><b>(.*?)</b>→ two matches:one,two— lazy stops at the first</b>
If you’re getting one huge match, greedy is why. If you’re getting too many small matches or the pattern stops mid-way, check that your closer isn’t appearing inside the content — a lazy group can’t know the difference between a real END and an END that happens to be text.
Negated classes beat lazy for single chars
For one-character delimiters, "([^"]*)" is better than "(.*?)": [^"] means “anything except the closer,” so the match cannot overshoot — a missing closer simply fails instead of leaking into the next quoted span. It’s also faster (no backtracking per character). Reserve .*? for multi-character delimiters like <!--(.*?)--> or word boundaries like \bSTART\b(.*?)\bEND\b.
In JavaScript
const text = 'cost [12] and [34]';
for (const m of text.matchAll(/\[([^\]]*)\]/g)) {
console.log(m[1]); // "12", then "34"
}
matchAll with the g flag yields every match with its captured group — m[1] is the inside text, m[0] is the whole match including brackets.
When the delimiters are regex-special
<, !, - are literal; but . * ? ( ) [ ] { } ^ $ | \ are not — escape them in the delimiter spots: matching text between ( and ) needs \( and \). And if the content itself can nest ((a(b)c)), regex is the wrong tool — JavaScript has no recursive matching; that’s a parser’s job, same as it is for nested grammar in general. More tokens explained in the cheat sheet.
Frequently asked questions
What's the regex to get text between two characters?
For single characters, use a negated class, not .*?: "([^"]*)" for double-quoted text, \(([^)]*)\) for parentheses, \[([^\]]*)\] for brackets. The captured group holds the inside text. The negated class [^"]* means "any chars except a quote", so the match can't accidentally swallow past the closer.
What's the regex to get text between two multi-character strings?
START(.*?)END — literal delimiters around a lazy group. The ? after * makes it stop at the first END instead of the last. Escape any regex metacharacters in the delimiters (e.g. text between [ and ] needs \[ and \]); ordinary characters like <!-- need none — write it straight as <!--(.*?)-->.
Why does my between-pattern match across the whole string?
Two usual causes. Greedy (.*) instead of lazy (.*?) — it extends to the last closer, so <b>a</b>x<b>b</b> yields one giant match. Or the delimiters contain regex metacharacters (. * [ () that you didn't escape, so they're matching things they shouldn't.
How do I exclude the delimiters from the match?
Wrap them in lookarounds: (?<=START).*?(?=END) — the match result is just the inside text. Caveat: JavaScript lookbehind must be fixed-length, so START can't contain +/*. Easier and portable: keep START(.*?)END and read capture group 1.
How do I match between strings across multiple lines?
Add the s (dotAll) flag so . matches newlines: /BEGIN(.*?)END/s. Without it, . stops at every \n and the match fails. The older portable idiom is [\s\S]*? — matches anything including line breaks in every JS engine since forever.
Can regex match nested delimiters, like parentheses inside parentheses?
No — JavaScript regex can't balance nesting (it has no recursion or balancing groups). \(([^)]*)\) on (a(b)c) matches only "(a(b" — it stops at the first closer. For real nested structures you need a parser, or match the outermost level and re-run inside.