Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Regular Expressions  Module 4 – Quantifiers: greedy, lazy and possessive

Greedy and lazy quantifiers

Why ".*" swallows two fields while ".*?" stops at the first, why lazy is not shortest, when a negated class wins, and what (?U) does in Go, PCRE2 and PHP.

  • Beginner
  • 25 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Go 1.27.1 and PHP 8.5.11
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Predict what a greedy and a lazy quantifier match on the same text, and how many characters each gives back or adds
  • Explain why a lazy quantifier still returns the leftmost match rather than the shortest one
  • Replace .*? with a negated class such as [^"]* when a field ends at one known character
  • Rewrite a pattern written for the ungreedy (?U) mode of Go, PCRE2 and PHP so that JavaScript and Python match the same text

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict what a greedy and a lazy quantifier match on the same text, and how many characters each gives back or adds
  • Explain why a lazy quantifier still returns the leftmost match rather than the shortest one
  • Replace .*? with a negated class such as [^"]* when a field ends at one known character
  • Rewrite a pattern written for the ungreedy (?U) mode of Go, PCRE2 and PHP so that JavaScript and Python match the same text

Before you start

On this page

Ask for the quoted fields of the row "Asha","Pune",411001 with ".*" and you get one match of 13 characters, "Asha","Pune", where you wanted two. The star in that pattern is greedy: it takes every character it can, so the match runs on to the last quote of the row. Add one question mark, ".*?", and the same row gives "Asha" and "Pune": lazy quantifiers take as little as they can.

Both searches are walked through below, one character at a time, followed by the case where lazy is still not shortest. Then .*? meets a negated class such as [^"]*, which says outright where a field ends and stays right on rows where the lazy pattern goes wrong. Last comes the ungreedy mode, (?U), which swaps the two forms in Go, PCRE2 and PHP and which JavaScript and Python refuse.

A greedy quantifier takes everything, then gives back

Every quantifier of the previous lesson, repetition with star, plus, question mark and braces, is greedy unless it says otherwise. A greedy quantifier: A quantifier in its usual form, such as * or {2,5}, which repeats its atom as many times as it can and gives repetitions back one at a time only when the rest of the pattern cannot match otherwise. repeats its atom: The single item a quantifier repeats, written right before it: one character, the dot, an escape, a character class or a group. In ab+ the atom is b; in (?:ab)+ it is the group ab. More in “Repetition with *, +, ? and {m,n}” as many times as it can, and only then lets the rest of the pattern try. When the rest fails, the engine does not leave that start position yet: it takes back one repetition and tries the rest again, and it goes on giving back until the rest matches or nothing is left to give. Going back to an earlier choice like this is backtracking: How a backtracking engine reacts to a failure: it returns to its most recent choice point, restores the text position it had there, and tries the next alternative. More in “How an engine finds a match, step by step”, the move that how an engine finds a match follows step by step. The ECMAScript specification fixes the order: a greedy repetition tries one more round of its atom first, and the rest of the pattern only when that round fails (ECMA-262, RepeatMatcher).

Here is what that means for ".*" on the row. Step through the search and watch the sub-label of .*, which says how many characters it holds:

Interactive · step by step How ".*" takes the whole row and gives characters back Step through one search: the greedy .* takes every character it can, then gives them back one at a time until the closing quote can match.
How ".*" takes the whole row and gives characters back, step 8 of 8. Showing The row, 20 characters, Opening quote " (matched at position 0), .* (greedy) (holds 11, gave back 8), Closing quote " (matched at position 12), Match at position 0. In focus: Match at position 0.The row, 20 characters"Asha","Pune",411001Opening quote "matched at position 0.* (greedy)holds 11, gave back 8Closing quote "matched at position 12Match at position 0"Asha","Pune"

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The pattern and the row. The pattern ".*" has three items: a quote, a greedy dot and star, and a second quote. It is tried on a row of two quoted fields and a number, "Asha","Pune",411001, 20 characters in all.
  2. The opening quote matches. The first item finds a quote at position 0, the start of the row, so the engine moves on to .* with the 19 characters that follow it.
  3. .* takes all 19 characters. A greedy quantifier repeats its atom as many times as it can before anything after it is tried. The dot matches every character of this row, so .* takes all 19, up to the end.
  4. No quote at the end. Now the closing quote is tried at the end of the row, where no character is left. The search does not give up: the engine goes back to the last choice it made, how many characters .* holds.
  5. Give back one character. .* gives back its last character, the final 1, and the closing quote is tried against it. A 1 is not a quote, so .* gives back one more.
  6. Keep giving back. The same happens for 0, 0, 1, 1, 4 and the comma. By now .* has given back seven characters one at a time, and the closing quote has failed on each of them.
  7. A quote follows at last. With .* holding 11 characters, the next character is the quote that closes Pune, at position 12. The closing quote matches it, and the whole pattern has matched.
  8. One match for two fields. The match runs from the first quote of the row to the last: "Asha","Pune", both fields and the comma between them. A greedy .* ends the match at the last quote it can reach, not at the first.

The dot can match every character of the row, so .* first takes all 19 characters after the opening quote, and the closing quote finds nothing left. .* gives back the six digits and the comma one at a time, and the closing quote fails on each of them; the eighth character it gives back is the quote after Pune, and the closing quote matches it. The program below plays the engine’s part in the same order and then asks the real regex. Press Run, then Edit: add a third field such as ,"Goa" to the end of the row and run it again, and the match grows to the new last quote.

JavaScript: what the greedy .* takes and gives back JavaScript · greedy-js/greedy.mjs
// How the greedy .* of ".*" finds where a quoted field ends. After the opening quote at position 0, .* takes the
// whole rest of the line, then gives back one character at a time until the closing quote can follow it. The loop
// plays the engine's part for this one pattern, in the order the ECMAScript specification gives; the last line asks
// the real regex.
const line = '"Asha","Pune",411001';

const rest = line.slice(1); // the characters .* may take after the opening quote
for (let take = rest.length; take >= 0; take--) {
  const next = rest[take];
  if (next === '"') {
    console.log(`.* holds ${take}, next is '"': the closing quote matches`);
    console.log(`gave back ${rest.length - take} characters; the match is ${line.slice(0, take + 2)}`);
    break;
  }
  console.log(`.* holds ${take}, next is ${next === undefined ? 'the end of the line' : `'${next}'`}: no quote`);
}

console.log('');
for (const match of line.matchAll(/".*"/g)) {
  console.log(`/".*"/g finds ${match[0]} at position ${match.index}`);
}

Output

.* holds 19, next is the end of the line: no quote
.* holds 18, next is '1': no quote
.* holds 17, next is '0': no quote
.* holds 16, next is '0': no quote
.* holds 15, next is '1': no quote
.* holds 14, next is '1': no quote
.* holds 13, next is '4': no quote
.* holds 12, next is ',': no quote
.* holds 11, next is '"': the closing quote matches
gave back 8 characters; the match is "Asha","Pune"

/".*"/g finds "Asha","Pune" at position 0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node greedy.mjs

Greedy does not mean careless: .* gave back exactly as much as the closing quote needed, not one character more. But it worked from the far end of the row, so it stopped at the last quote it could reach, and the comma between the two fields ended up inside the match.

Key idea

A greedy quantifier takes every repetition it can and gives them back one at a time, from the end, only as far as the rest of the pattern needs; between two quotes it stops at the last quote it can reach.

A lazy quantifier takes as little as it can, then extends

Put a question mark after the quantifier and its preference turns round. A lazy quantifier: A quantifier followed by a question mark, such as *? or {2,5}?, which repeats its atom as few times as it can and adds one repetition at a time only when the rest of the pattern cannot match otherwise. Also called non-greedy or reluctant. repeats its atom as few times as it can, lets the rest of the pattern try at once, and takes one more repetition only when the rest fails. The counts stay the same: *? still allows zero or more, +? one or more. Only the order of the tries changes: once its minimum is met, a non-greedy repetition tries the rest of the pattern before each further round of its atom (ECMA-262, RepeatMatcher, step 9). Python’s documentation calls the same thing non-greedy or minimal matching, and Java’s calls it reluctant.

Every quantifier has a lazy form, and with nothing after it in the pattern each one stops at its minimum. The table lists what each form took from aaaa in the program further down:

Greedy form Takes from aaaa Lazy form Takes from aaaa
a* aaaa a*? nothing
a+ aaaa a+? a
a? a a?? nothing
a{2,3} aaa a{2,3}? aa

Now the same row with ".*?". Step through it and compare the sub-label of .*? with the greedy run:

Interactive · step by step How ".*?" takes one character at a time Step through the same search with a lazy .*?: it takes nothing at first and one more character each time the closing quote fails.
How ".*?" takes one character at a time, step 8 of 8. Showing The row, 20 characters, Opening quote " (matched at position 7), .*? (lazy) (holds 4), Closing quote " (matched at position 12), Matches ("Asha" at 0, "Pune" at 7).The row, 20 characters"Asha","Pune",411001Opening quote "matched at position 7.*? (lazy)holds 4Closing quote "matched at position 12Matches"Asha" at 0, "Pune" at 7

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The same row, a lazy middle. Now a question mark follows the star, which makes the middle of the pattern lazy. The pattern ".*?" is tried on the same 20 characters, "Asha","Pune",411001.
  2. The opening quote matches. As before, the first item finds the quote at position 0, and the engine moves on to the quantifier.
  3. .*? takes nothing at first. A lazy quantifier repeats its atom as few times as it can. .*? starts with no characters at all and lets the closing quote try at once, against the A of Asha. That fails.
  4. One more character. Only when the rest of the pattern fails does .*? take one more character. It takes the A, and the closing quote fails again, on s.
  5. And one more each time. The same for s and h: each time the quote fails, .*? takes one character more, and the quote is tried on the next one, the final a of Asha.
  6. The quote matches. With 4 characters held, the next character is the quote after Asha, at position 5. The closing quote matches, and the first match is "Asha": .*? took only what the rest of the pattern needed.
  7. The search goes on. A search for every match starts again after the first one, at position 6. The comma there is not a quote. At position 7 a quote starts the second match, and four characters later "Pune" is complete.
  8. No quote is left. From position 13 the rest of the row is ,411001, which holds no quote, so no third match starts. The lazy pattern found two matches, one for each field.

.*? held 0, 1, 2, 3 and then 4 characters, and the closing quote matched on the fifth try, so the first match is "Asha". The search then started again after it and found "Pune" the same way. The program prints those tries, every match of the row, and every form of the table. Press Edit and try /".+?"/g: it still finds both fields, since each holds at least one character.

JavaScript: what the lazy .*? takes, one character at a time JavaScript · lazy-js/lazy.mjs
// How the lazy .*? of ".*?" finds where a quoted field ends. After the opening quote, .*? takes nothing at first and
// lets the closing quote try at once; each time the quote fails, it takes one more character. The loop plays the
// engine's part for the first field; then the real regex finds every field, and every lazy form shows its minimum.
const line = '"Asha","Pune",411001';

const rest = line.slice(1);
for (let take = 0; take <= rest.length; take++) {
  const next = rest[take];
  if (next === '"') {
    console.log(`.*? holds ${take}, next is '"': the closing quote matches`);
    console.log(`took ${take} characters; the match is ${line.slice(0, take + 2)}`);
    break;
  }
  console.log(`.*? holds ${take}, next is ${next === undefined ? 'the end of the line' : `'${next}'`}: take one more`);
}

console.log('');
for (const match of line.matchAll(/".*?"/g)) {
  console.log(`/".*?"/g finds ${match[0]} at position ${match.index}`);
}
const alone = line.match(/".*?/g);
console.log(`/".*?/g, with nothing after the lazy part: ${alone.length} matches, each ${alone[0]}`);

console.log('');
for (const re of [/a*/, /a*?/, /a+/, /a+?/, /a?/, /a??/, /a{2,3}/, /a{2,3}?/]) {
  console.log(`${String(re).padEnd(10)} on aaaa takes ${JSON.stringify('aaaa'.match(re)[0])}`);
}

Output

.*? holds 0, next is 'A': take one more
.*? holds 1, next is 's': take one more
.*? holds 2, next is 'h': take one more
.*? holds 3, next is 'a': take one more
.*? holds 4, next is '"': the closing quote matches
took 4 characters; the match is "Asha"

/".*?"/g finds "Asha" at position 0
/".*?"/g finds "Pune" at position 7
/".*?/g, with nothing after the lazy part: 4 matches, each "

/a*/       on aaaa takes "aaaa"
/a*?/      on aaaa takes ""
/a+/       on aaaa takes "aaaa"
/a+?/      on aaaa takes "a"
/a?/       on aaaa takes "a"
/a??/      on aaaa takes ""
/a{2,3}/   on aaaa takes "aaa"
/a{2,3}?/  on aaaa takes "aa"

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lazy.mjs

A lazy quantifier at the end of a pattern

Wrong: /".*?/g to take a quoted field up to its end. With nothing after it, .*? is content with zero characters, so each match is a lone quote, four of them on the row, as the program shows. Right: say where the field ends, /".*?"/g, so the closing quote forces .*? to extend.

The two runs differ in one thing only, the end they approach the closing quote from: the greedy star from the end of the row, the lazy one from the opening quote.

Key idea

A lazy quantifier tries the rest of the pattern before each extra repetition, so between two quotes it stops at the first closing quote that lets the whole pattern match.

Checkpoint

Check: Greedy repetition gives back · Lazy repetition extends

  1. Question 1 of 2 ".*" runs on the row "Pune","Goa",12. How many characters does .* give back before the closing quote matches?

    Type a number.

    A hint

    The closing quote first fails at the end of the row. Which characters stand between the end and the last quote of the row?

    Show the answer: Question 1 of 2

    Answer: 4 characters

    After the opening quote, .* takes all 14 characters up to the end of the row, and the closing quote fails there. .* gives back the 2, then the 1, then the comma, and the closing quote fails on each of them. The fourth character it gives back is the quote after Goa, and the closing quote matches it, so the match is "Pune","Goa". The lesson's row gave back 8 characters in the same way.

  2. Question 2 of 2 In JavaScript, /".*?"/g runs on the row "Pune","Goa",12. What does match return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Two matches, "Pune" and "Goa"

    The lazy walk of the lesson does exactly this on its own row: .*? holds 0, 1, 2, 3 and then 4 characters, the closing quote matches, and the search starts again after the match. Here the two matches are "Pune" at position 0 and "Goa" at position 7.

Lazy is not shortest: the leftmost start wins first

Lazy quantifiers are easy to read as “find the shortest match”. They are not. Search xxy with x.*?y and the answer is xxy, all three characters, although xy, which starts one position later, is shorter. The engine tries start positions from left to right and keeps the first one at which the whole pattern matches; that rule comes before any preference of a quantifier. At position 0 the x matches, .*? takes nothing and y fails against the second x; .*? takes that x, y matches, and the search is over. It never looks at position 1. MDN states the same rule: once a match succeeds at some index, later indices are not tried.

Laziness only decides how much a match takes from the start the engine has already chosen. Run the six searches; the last three are the familiar case of tags:

JavaScript: lazy, greedy and a negated class, start by start JavaScript · leftmost-js/leftmost.mjs
// A lazy quantifier does not look for the shortest match in the whole text. The engine tries start positions from the
// left and keeps the first one where the pattern matches; laziness only decides how much that match takes.
const tries = [
  [/x.*?y/g, 'xxy'],
  [/x.*y/g, 'xxy'],
  [/x[^x]*y/g, 'xxy'],
  [/<.+?>/g, '<a><b>'],
  [/<.+>/g, '<a><b>'],
  [/<[^>]+>/g, '<a><b>'],
];
for (const [re, text] of tries) {
  const found = [...text.matchAll(re)].map((m) => `${m[0]} at ${m.index}`);
  console.log(`${String(re).padEnd(11)} on ${text.padEnd(7)} finds ${found.join(', ')}`);
}

Output

/x.*?y/g    on xxy     finds xxy at 0
/x.*y/g     on xxy     finds xxy at 0
/x[^x]*y/g  on xxy     finds xy at 1
/<.+?>/g    on <a><b>  finds <a> at 0, <b> at 3
/<.+>/g     on <a><b>  finds <a><b> at 0
/<[^>]+>/g  on <a><b>  finds <a> at 0, <b> at 3

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node leftmost.mjs

Read the rows in pairs. On xxy the lazy and the greedy pattern find the same match, and only x[^x]*y finds xy: its class refuses a second x, so no match can start at position 0. On <a><b> the lazy <.+?> finds the two tags, the greedy <.+> one match that spans both, and <[^>]+> the two tags without any giving back. JavaScript, Python and Go’s default are all leftmost-first: A rule for choosing among matches: take the earliest start, then the match a backtracking search finds first, which follows the order of the alternatives. JavaScript, Python and PCRE2 use it. More in “How an engine finds a match, step by step” engines, and the Python and Go tabs find the same matches. Go also has Longest(), which switches a compiled pattern to the leftmost-longest: A rule for choosing among matches: take the earliest start, then the longest match from it, whatever the order of the alternatives. POSIX specifies it. More in “How an engine finds a match, step by step” rule; the last two lines of the Go tab show what it does to a lazy quantifier:

Python and Go: the same searches, and Go's Longest()

Python 3.14.8 · leftmost/leftmost.py

"""The leftmost searches in Python's re: a lazy quantifier still returns the match that starts furthest left."""

import re

for pattern, text in [
    (r"x.*?y", "xxy"),
    (r"x.*y", "xxy"),
    (r"x[^x]*y", "xxy"),
    (r"<.+?>", "<a><b>"),
    (r"<.+>", "<a><b>"),
    (r"<[^>]+>", "<a><b>"),
]:
    found = [f"{m.group()} at {m.start()}" for m in re.finditer(pattern, text)]
    print(f"{pattern:9} on {text:7} finds {', '.join(found)}")

Output

x.*?y     on xxy     finds xxy at 0
x.*y      on xxy     finds xxy at 0
x[^x]*y   on xxy     finds xy at 1
<.+?>     on <a><b>  finds <a> at 0, <b> at 3
<.+>      on <a><b>  finds <a><b> at 0
<[^>]+>   on <a><b>  finds <a> at 0, <b> at 3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 leftmost.py

Go 1.27.1 · leftmost/leftmost.go

// The leftmost searches in Go's regexp package (leftmost-first, like JavaScript and Python), then two lazy patterns
// after Longest(), which switches the Regexp to leftmost-longest: among the matches that start furthest left it takes
// the longest, so the lazy quantifier no longer makes a difference.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func show(note string, re *regexp.Regexp, text string) {
	var found []string
	for _, loc := range re.FindAllStringIndex(text, -1) {
		found = append(found, fmt.Sprintf("%s at %d", text[loc[0]:loc[1]], loc[0]))
	}
	fmt.Printf("%s%-9s on %-7s finds %s\n", note, re, text, strings.Join(found, ", "))
}

func main() {
	for _, t := range [][2]string{
		{`x.*?y`, "xxy"},
		{`x.*y`, "xxy"},
		{`x[^x]*y`, "xxy"},
		{`<.+?>`, "<a><b>"},
		{`<.+>`, "<a><b>"},
		{`<[^>]+>`, "<a><b>"},
	} {
		show("", regexp.MustCompile(t[0]), t[1])
	}
	fmt.Println()
	for _, t := range [][2]string{{`x.*?y`, "xxy"}, {`<.+?>`, "<a><b>"}} {
		re := regexp.MustCompile(t[0])
		re.Longest()
		show("Longest(): ", re, t[1])
	}
}

Output

x.*?y     on xxy     finds xxy at 0
x.*y      on xxy     finds xxy at 0
x[^x]*y   on xxy     finds xy at 1
<.+?>     on <a><b>  finds <a> at 0, <b> at 3
<.+>      on <a><b>  finds <a><b> at 0
<[^>]+>   on <a><b>  finds <a> at 0, <b> at 3

Longest(): x.*?y     on xxy     finds xxy at 0
Longest(): <.+?>     on <a><b>  finds <a><b> at 0

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run leftmost.go

After Longest(), <.+?> finds <a><b> again: among the matches that start furthest left, Go now takes the longest one (its documentation of Longest), so a lazy quantifier makes no difference there. Lazy and greedy are preferences that only a leftmost-first search follows.

Key idea

Laziness decides how much a match takes, never where it starts: the leftmost start wins first, so x.*?y on xxy returns xxy.

Checkpoint

Check: The leftmost start comes first

  1. One question What does /a.*?b/ find in the text aab?

    Choose one answer.

    Show the answer: One question

    Answer: aab, starting at position 0

    Laziness only decides how much .*? takes from the start the engine has already chosen. The leftmost start comes first, so aab, three characters, is returned, just as x.*?y returns xxy in the lesson.

A negated class says where a field ends

On the clean row, ".*?" and "[^"]*" find the same two fields. The second is a negated class: A character class that starts with a caret right after the opening bracket, such as [^0-9]. It matches exactly one character that is not in the set, line breaks included. More in “Character classes, ranges and negation” repeated by a greedy star: any run of characters that are not a quote. It cannot run past the closing quote, because a quote is the one character it refuses, while a lazy dot has no such limit: it extends past a closing quote whenever the rest of the pattern needs it to. Two rows show the difference. The first is a record whose keys are quoted fields followed by a colon; the second is a CSV row whose address field holds a line break, which CSV allows inside quotes (RFC 4180, section 2, rule 6):

JavaScript: three ways to match a quoted field JavaScript · fields-js/fields.mjs
// Three ways to write the middle of a quoted field, on three lines: a clean row, the keys of a JSON-like record (a key
// is a quoted field followed by a colon) and a CSV row whose second field holds a line break. Each match is printed in
// [brackets], with a line break shown as \n.
const show = (re, text) => {
  const found = text.match(re) ?? [];
  const list = found.map((m) => `[${m.replace(/\n/g, '\\n')}]`).join(' ');
  console.log(`  ${String(re).padEnd(11)} ${list || 'no match'}`);
};

const row = '"Asha","Pune",411001';
console.log('clean row:');
for (const re of [/".*"/g, /".*?"/g, /"[^"]*"/g]) show(re, row);

const record = '{"name": "Asha Rao", "city": "Pune"}';
console.log('keys of a record:');
for (const re of [/".*?":/g, /"[^"]*":/g]) show(re, record);

const csv = '"Asha Rao","12 MG Road\nPune","411001"';
console.log('a line break inside a field:');
for (const re of [/".*?"/g, /".*?"/gs, /"[^"]*"/g]) show(re, csv);

Output

clean row:
  /".*"/g     ["Asha","Pune"]
  /".*?"/g    ["Asha"] ["Pune"]
  /"[^"]*"/g  ["Asha"] ["Pune"]
keys of a record:
  /".*?":/g   ["name":] ["Asha Rao", "city":]
  /"[^"]*":/g ["name":] ["city":]
a line break inside a field:
  /".*?"/g    ["Asha Rao"] [","]
  /".*?"/gs   ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]
  /"[^"]*"/g  ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node fields.mjs

On the record, ".*?": finds "name": and then "Asha Rao", "city":. After "Asha Rao" the colon failed against a comma, so .*? took one more character, and another, past the closing quote of Asha Rao and the opening quote of city, until a quote and a colon followed. The negated class cannot do that: its run stops at the next quote, so from the quote before Asha Rao the colon fails and the engine moves on to a later start. The second key comes out right, "city":.

On the CSV row the dot fails the other way: it does not match a line break, so ".*?" cannot finish the address field and ends up pairing the two quotes around a comma, ",". The s flag lets the dot cross the line break and mends this row, but it does nothing for the record; the class mends both. Python, Go and PHP print the same matches:

Python, Go and PHP: the same three rows

Python 3.14.8 · fields/fields.py

"""The three ways to write the middle of a quoted field, in Python's re, on the same three lines. Each match is
printed in [brackets], with a line break shown as \\n; re.S is Python's flag for a dot that also matches it."""

import re


def show(pattern, text, flags=0, note=""):
    found = re.findall(pattern, text, flags)
    shown = " ".join(f"[{m}]".replace("\n", "\\n") for m in found)
    print(f"  {pattern + note:11} {shown or 'no match'}")


row = '"Asha","Pune",411001'
print("clean row:")
for pattern in [r'".*"', r'".*?"', r'"[^"]*"']:
    show(pattern, row)

record = '{"name": "Asha Rao", "city": "Pune"}'
print("keys of a record:")
for pattern in [r'".*?":', r'"[^"]*":']:
    show(pattern, record)

csv = '"Asha Rao","12 MG Road\nPune","411001"'
print("a line break inside a field:")
show(r'".*?"', csv)
show(r'".*?"', csv, re.S, " re.S")
show(r'"[^"]*"', csv)

Output

clean row:
  ".*"        ["Asha","Pune"]
  ".*?"       ["Asha"] ["Pune"]
  "[^"]*"     ["Asha"] ["Pune"]
keys of a record:
  ".*?":      ["name":] ["Asha Rao", "city":]
  "[^"]*":    ["name":] ["city":]
a line break inside a field:
  ".*?"       ["Asha Rao"] [","]
  ".*?" re.S  ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]
  "[^"]*"     ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fields.py

Go 1.27.1 · fields/fields.go

// The three ways to write the middle of a quoted field, in Go's regexp package (RE2 syntax), on the same three lines.
// Each match is printed in [brackets], with a line break shown as \n; (?s) lets the dot match a line break too.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func show(pattern, text string) {
	var shown []string
	for _, m := range regexp.MustCompile(pattern).FindAllString(text, -1) {
		shown = append(shown, "["+strings.ReplaceAll(m, "\n", `\n`)+"]")
	}
	if len(shown) == 0 {
		shown = []string{"no match"}
	}
	fmt.Printf("  %-11s %s\n", pattern, strings.Join(shown, " "))
}

func main() {
	row := `"Asha","Pune",411001`
	fmt.Println("clean row:")
	for _, p := range []string{`".*"`, `".*?"`, `"[^"]*"`} {
		show(p, row)
	}

	record := `{"name": "Asha Rao", "city": "Pune"}`
	fmt.Println("keys of a record:")
	for _, p := range []string{`".*?":`, `"[^"]*":`} {
		show(p, record)
	}

	csv := "\"Asha Rao\",\"12 MG Road\nPune\",\"411001\""
	fmt.Println("a line break inside a field:")
	for _, p := range []string{`".*?"`, `(?s)".*?"`, `"[^"]*"`} {
		show(p, csv)
	}
}

Output

clean row:
  ".*"        ["Asha","Pune"]
  ".*?"       ["Asha"] ["Pune"]
  "[^"]*"     ["Asha"] ["Pune"]
keys of a record:
  ".*?":      ["name":] ["Asha Rao", "city":]
  "[^"]*":    ["name":] ["city":]
a line break inside a field:
  ".*?"       ["Asha Rao"] [","]
  (?s)".*?"   ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]
  "[^"]*"     ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run fields.go

PHP 8.5.11 · fields/fields.php

<?php
// The three ways to write the middle of a quoted field, in PHP's preg functions (PCRE2), on the same three lines.
// Each match is printed in [brackets], with a line break shown as \n; the s modifier lets the dot match it too.
function show(string $pattern, string $text): void
{
    preg_match_all($pattern, $text, $found);
    $shown = array_map(fn ($m) => '[' . str_replace("\n", '\n', $m) . ']', $found[0]);
    printf("  %-11s %s\n", $pattern, $shown ? implode(' ', $shown) : 'no match');
}

$row = '"Asha","Pune",411001';
echo "clean row:\n";
foreach (['/".*"/', '/".*?"/', '/"[^"]*"/'] as $p) {
    show($p, $row);
}

$record = '{"name": "Asha Rao", "city": "Pune"}';
echo "keys of a record:\n";
foreach (['/".*?":/', '/"[^"]*":/'] as $p) {
    show($p, $record);
}

$csv = "\"Asha Rao\",\"12 MG Road\nPune\",\"411001\"";
echo "a line break inside a field:\n";
foreach (['/".*?"/', '/".*?"/s', '/"[^"]*"/'] as $p) {
    show($p, $csv);
}

Output

clean row:
  /".*"/      ["Asha","Pune"]
  /".*?"/     ["Asha"] ["Pune"]
  /"[^"]*"/   ["Asha"] ["Pune"]
keys of a record:
  /".*?":/    ["name":] ["Asha Rao", "city":]
  /"[^"]*":/  ["name":] ["city":]
a line break inside a field:
  /".*?"/     ["Asha Rao"] [","]
  /".*?"/s    ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]
  /"[^"]*"/   ["Asha Rao"] ["12 MG Road\nPune"] ["411001"]

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php fields.php

The class saves work as well. A lazy .*? tries the closing quote after every character of the field, and a greedy .* first runs to the end of the line and then gives back. PCRE2 can count this: its test program finds the smallest match limit each match needs, the number of rounds of its matching loop, which, unlike a time, does not depend on how fast the computer is (pcre2api, pcre2test). Each subject below holds a field of 1,000 characters:

PCRE2 10.49: the work of each pattern on a field of 1,000 characters

PCRE2 10.49 · cost/cost_pcre2.py

"""How much work PCRE2 10.49 does to match one quoted field of 1,000 characters with ".*", ".*?" and "[^"]*".

pcre2test's find_limits_noheap modifier runs the match again and again to find the smallest match limit that lets it
finish: the number of rounds of PCRE2's matching loop the match needs (pcre2api), a count that comes out the same on
every computer, unlike a time. In a subject line, \\[a]{1000} is pcre2test's way to write 1,000 copies of a; this
program prints such a run in the same short form, a{1000}, where pcre2test printed the whole match."""

import contextlib
import io
import re

from pcre2_session import run


def show(script: str) -> None:
    """Runs the script and prints the transcript with every run of ten or more equal letters written as a{1000}."""
    out = io.StringIO()
    with contextlib.redirect_stdout(out):
        run(script)
    text = re.sub(r"([a-z])\1{9,}", lambda m: f"{m.group(1)}{{{len(m.group(0))}}}", out.getvalue())
    print(text, end="")


show(r'''
# A quoted field of 1,000 characters at the end of the line
/".*"/
    "\[a]{1000}"\=find_limits_noheap

/".*?"/
    "\[a]{1000}"\=find_limits_noheap

/"[^"]*"/
    "\[a]{1000}"\=find_limits_noheap

# The same field with 1,000 more characters after it
/".*"/
    "\[a]{1000}"\[b]{1000}\=find_limits_noheap

/".*?"/
    "\[a]{1000}"\[b]{1000}\=find_limits_noheap

/"[^"]*"/
    "\[a]{1000}"\[b]{1000}\=find_limits_noheap
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
# A quoted field of 1,000 characters at the end of the line
/".*"/
    "\[a]{1000}"\=find_limits_noheap
Minimum match limit = 4
Minimum depth limit = 3
 0: "a{1000}"

/".*?"/
    "\[a]{1000}"\=find_limits_noheap
Minimum match limit = 1003
Minimum depth limit = 3
 0: "a{1000}"

/"[^"]*"/
    "\[a]{1000}"\=find_limits_noheap
Minimum match limit = 2
Minimum depth limit = 2
 0: "a{1000}"

# The same field with 1,000 more characters after it
/".*"/
    "\[a]{1000}"\[b]{1000}\=find_limits_noheap
Minimum match limit = 1004
Minimum depth limit = 3
 0: "a{1000}"

/".*?"/
    "\[a]{1000}"\[b]{1000}\=find_limits_noheap
Minimum match limit = 1003
Minimum depth limit = 3
 0: "a{1000}"

/"[^"]*"/
    "\[a]{1000}"\[b]{1000}\=find_limits_noheap
Minimum match limit = 2
Minimum depth limit = 2
 0: "a{1000}"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cost_pcre2.py

cost/pcre2_session.py

"""Runs a script of patterns and subjects through pcre2test, the test program of the PCRE2 10.49 library, and prints
what it printed.

pcre2test reads a pattern line such as /a.b/ (with modifiers after the closing slash, for example /a.b/dotall,utf),
then one subject per indented line, and prints each subject with " 0:" and the matched text (" 1:" and on for groups)
or "No match". A line starting with # is a comment and is printed as it is. Escapes in a subject such as \\r, \\n or
\\x{2028} stand for those characters (pcre2test's own subject escapes), so invisible characters can be tested.

The script is given in the example file itself, so the example's run record changes whenever the script does:

    from pcre2_session import run
    run(r'''
    /a.b/
        a\\rb
    ''')

The version banner and its release date are not printed (pcre2test -q); the session checks the version itself and
stops if this computer's pcre2test is not PCRE2 10.49, so a record never shows another version's behaviour.
"""

import re
import subprocess
import sys
import tempfile
from pathlib import Path

VERSION = "10.49"
_CANDIDATES = ("/opt/homebrew/bin/pcre2test", "/usr/local/bin/pcre2test")
_ENV = {"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "LANG": "C.UTF-8", "TZ": "UTC"}


def _tool() -> str:
    for candidate in _CANDIDATES:
        if Path(candidate).is_file():
            return candidate
    sys.exit("pcre2test (PCRE2 10.49) was not found")


def _check_version(tool: str) -> None:
    first = subprocess.run([tool, "-C"], capture_output=True, text=True, env=_ENV).stdout.splitlines()[:1]
    found = re.match(r"PCRE2 version (\S+)", first[0]) if first else None
    if not found or found.group(1) != VERSION:
        sys.exit(f"this session needs PCRE2 {VERSION}, found: {first[0] if first else 'nothing'}")


def run(script: str) -> None:
    """Prints the pcre2test transcript of the script (patterns, subjects and results)."""
    tool = _tool()
    _check_version(tool)
    lines = script.strip("\n").splitlines()
    text = "\n".join(_dedent(lines)) + "\n"
    with tempfile.TemporaryDirectory() as folder:
        source = Path(folder) / "patterns.txt"
        source.write_text(text, encoding="utf-8")
        done = subprocess.run([tool, "-q", str(source)], capture_output=True, text=True, encoding="utf-8", env=_ENV)
    print(f"$ pcre2test -q patterns.txt   (PCRE2 {VERSION})")
    sys.stdout.write(done.stdout)
    if done.stderr:
        sys.stdout.write(done.stderr)
    if done.returncode != 0:
        print(f"[exit status {done.returncode}]")
    sys.stdout.flush()


def _dedent(lines: list[str]) -> list[str]:
    """Removes the indentation the whole script shares, so it can sit indented inside a Python call."""
    widths = [len(line) - len(line.lstrip(" ")) for line in lines if line.strip()]
    cut = min(widths) if widths else 0
    return [line[cut:] for line in lines]

The minimum match limits from the run, side by side:

Subject ".*" ".*?" "[^"]*"
the field, then the end of the line 4 1,003 2
the field, then 1,000 more characters 1,004 1,003 2

The lazy pattern pays for the length of the field and the greedy one for the text after it; the class pays for neither. PCRE2 even compiles [^"]* as a run that never gives characters back, since the quote after it can never be one of them (pcre2api calls this auto-possessification); the next lesson explains such runs. On a long line with many fields the difference adds up, and some patterns make a backtracking engine try far more combinations than these; a later module, on performance and ReDoS, deals with them.

When a field ends at one character

Write the field as that character, a negated class of it and the character again: "[^"]*", <[^>]*>, \[[^\]]*\]. Keep .*? for ends longer than one character, and use a CSV, JSON or HTML parser when the format has escapes, such as the doubled quote "" that CSV writes inside a field (RFC 4180, rule 7).

Checkpoint

Check: A negated class for a known end

  1. Question 1 of 2 Which pattern finds exactly the two keys "id": and "ok": in the record {"id": "7", "ok": true}?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: /"[^"]*":/g

    The record works like the lesson's: a lazy dot may run past a closing quote whenever the rest of the pattern fails there, and a negated class may not. Only "[^"]*": keeps each match inside one field.

  2. Question 2 of 2 A CSV field holds a line break inside its quotes: "12 MG Road on one line and Pune" on the next. Which patterns match that whole field in JavaScript?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • /".*?"/gs
    • /"[^"]*"/g

    CSV allows a line break inside a quoted field (RFC 4180). Of the two patterns that cross it, the class also stays right on rows where a lazy dot overruns a closing quote, which is why the lesson prefers it.

Ungreedy mode swaps the two forms, where it exists

Some engines let a pattern flip its default. In Go, PCRE2 and PHP, (?U) at the start of a pattern turns on ungreedy mode: An option of PCRE2, PHP (the U modifier) and Go, also set with (?U) in a pattern, that swaps the two forms: a quantifier without a question mark is lazy and one with it is greedy. JavaScript and Python have no such mode.: every quantifier without a question mark becomes lazy, and every quantifier with one becomes greedy. Go’s syntax table describes the flag as a swap of x* and x*?, x+ and x+?, and so on; PCRE2 names it PCRE2_UNGREEDY and says it is not compatible with Perl; PHP also sets it with the U modifier after the closing delimiter. Each tab runs the row through both forms, with and without the mode:

Go, PHP, PCRE2 and Python: what (?U) does

Go 1.27.1 · ungreedy/ungreedy.go

// (?U) in Go's regexp package (RE2 syntax) is the ungreedy flag: it swaps the meaning of x* and x*?, x+ and x+?, and
// of the other quantifiers.
package main

import (
	"fmt"
	"regexp"
	"strings"
)

func main() {
	line := `"Asha","Pune",411001`
	for _, p := range []string{`".*"`, `(?U)".*"`, `".*?"`, `(?U)".*?"`, `(?U)\d+`, `(?U)\d+?`} {
		found := regexp.MustCompile(p).FindAllString(line, -1)
		fmt.Printf("%-10s %s\n", p, strings.Join(found, " "))
	}
}

Output

".*"       "Asha","Pune"
(?U)".*"   "Asha" "Pune"
".*?"      "Asha" "Pune"
(?U)".*?"  "Asha","Pune"
(?U)\d+    4 1 1 0 0 1
(?U)\d+?   411001

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run ungreedy.go

PHP 8.5.11 · ungreedy/ungreedy.php

<?php
// The U modifier of PHP's preg functions, and (?U) inside a pattern, set PCRE2's ungreedy mode: a quantifier without
// a question mark is lazy, and one with a question mark is greedy.
$line = '"Asha","Pune",411001';
foreach (['/".*"/', '/".*"/U', '/(?U)".*"/', '/".*?"/U', '/\d+/U', '/\d+?/U'] as $pattern) {
    preg_match_all($pattern, $line, $found);
    printf("%-11s %s\n", $pattern, implode(' ', $found[0]));
}

Output

/".*"/      "Asha","Pune"
/".*"/U     "Asha" "Pune"
/(?U)".*"/  "Asha" "Pune"
/".*?"/U    "Asha","Pune"
/\d+/U      4 1 1 0 0 1
/\d+?/U     411001

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php ungreedy.php

PCRE2 10.49 · ungreedy/ungreedy_pcre2.py

"""Ungreedy mode in PCRE2 10.49 through pcre2test: (?U) at the start of the pattern, or the ungreedy modifier, which
sets the PCRE2_UNGREEDY compile option. Either way ".*" becomes lazy and ".*?" greedy."""

from pcre2_session import run

run(r'''
/".*"/g
    "Asha","Pune",411001

/(?U)".*"/g
    "Asha","Pune",411001

/".*"/g,ungreedy
    "Asha","Pune",411001

/".*?"/g,ungreedy
    "Asha","Pune",411001
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
/".*"/g
    "Asha","Pune",411001
 0: "Asha","Pune"

/(?U)".*"/g
    "Asha","Pune",411001
 0: "Asha"
 0: "Pune"

/".*"/g,ungreedy
    "Asha","Pune",411001
 0: "Asha"
 0: "Pune"

/".*?"/g,ungreedy
    "Asha","Pune",411001
 0: "Asha","Pune"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ungreedy_pcre2.py

Python 3.14.8 · ungreedy/ungreedy.py

"""Python's re has no ungreedy mode: (?U) is refused, and none of its flags swaps greedy and lazy."""

import re

for source in ['(?U)".*"', '(?U:".*")']:
    try:
        re.compile(source)
        print(f"{source:10} compiles")
    except re.PatternError as e:
        print(f"{source:10} {type(e).__name__}: {e}")

print("flags:", ", ".join(sorted(flag.name for flag in re.RegexFlag)))

Output

(?U)".*"   PatternError: unknown extension ?U at position 1
(?U:".*")  PatternError: unknown extension ?U at position 1
flags: ASCII, DEBUG, DOTALL, IGNORECASE, LOCALE, MULTILINE, UNICODE, VERBOSE

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ungreedy.py

ungreedy/pcre2_session.py

"""Runs a script of patterns and subjects through pcre2test, the test program of the PCRE2 10.49 library, and prints
what it printed.

pcre2test reads a pattern line such as /a.b/ (with modifiers after the closing slash, for example /a.b/dotall,utf),
then one subject per indented line, and prints each subject with " 0:" and the matched text (" 1:" and on for groups)
or "No match". A line starting with # is a comment and is printed as it is. Escapes in a subject such as \\r, \\n or
\\x{2028} stand for those characters (pcre2test's own subject escapes), so invisible characters can be tested.

The script is given in the example file itself, so the example's run record changes whenever the script does:

    from pcre2_session import run
    run(r'''
    /a.b/
        a\\rb
    ''')

The version banner and its release date are not printed (pcre2test -q); the session checks the version itself and
stops if this computer's pcre2test is not PCRE2 10.49, so a record never shows another version's behaviour.
"""

import re
import subprocess
import sys
import tempfile
from pathlib import Path

VERSION = "10.49"
_CANDIDATES = ("/opt/homebrew/bin/pcre2test", "/usr/local/bin/pcre2test")
_ENV = {"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "LANG": "C.UTF-8", "TZ": "UTC"}


def _tool() -> str:
    for candidate in _CANDIDATES:
        if Path(candidate).is_file():
            return candidate
    sys.exit("pcre2test (PCRE2 10.49) was not found")


def _check_version(tool: str) -> None:
    first = subprocess.run([tool, "-C"], capture_output=True, text=True, env=_ENV).stdout.splitlines()[:1]
    found = re.match(r"PCRE2 version (\S+)", first[0]) if first else None
    if not found or found.group(1) != VERSION:
        sys.exit(f"this session needs PCRE2 {VERSION}, found: {first[0] if first else 'nothing'}")


def run(script: str) -> None:
    """Prints the pcre2test transcript of the script (patterns, subjects and results)."""
    tool = _tool()
    _check_version(tool)
    lines = script.strip("\n").splitlines()
    text = "\n".join(_dedent(lines)) + "\n"
    with tempfile.TemporaryDirectory() as folder:
        source = Path(folder) / "patterns.txt"
        source.write_text(text, encoding="utf-8")
        done = subprocess.run([tool, "-q", str(source)], capture_output=True, text=True, encoding="utf-8", env=_ENV)
    print(f"$ pcre2test -q patterns.txt   (PCRE2 {VERSION})")
    sys.stdout.write(done.stdout)
    if done.stderr:
        sys.stdout.write(done.stderr)
    if done.returncode != 0:
        print(f"[exit status {done.returncode}]")
    sys.stdout.flush()


def _dedent(lines: list[str]) -> list[str]:
    """Removes the indentation the whole script shares, so it can sit indented inside a Python call."""
    widths = [len(line) - len(line.lstrip(" ")) for line in lines if line.strip()]
    cut = min(widths) if widths else 0
    return [line[cut:] for line in lines]

In Go, (?U)".*" finds the two fields and (?U)".*?" finds one match across both, the reverse of the same patterns without the flag, and (?U)\d+ takes one digit at a time: six matches in 411001. PHP’s /".*"/U and PCRE2’s ungreedy modifier print the same lines. Python’s re stops with a PatternError, unknown extension ?U, and none of its flags swaps the two forms. JavaScript refuses the mode too:

JavaScript: no (?U), and the same patterns swapped by hand JavaScript · ungreedy-js/ungreedy.mjs
// (?U), the ungreedy mode of Go, PCRE2 and PHP, is not part of JavaScript: a pattern that uses it does not compile.
// A pattern written for that mode is ported by swapping each quantifier by hand: in ungreedy mode .* is lazy and .*?
// is greedy, so the JavaScript versions are the other way round.
for (const source of ['(?U)".*"', '(?U:".*")']) {
  try {
    new RegExp(source);
    console.log(`${source.padEnd(10)} compiles`);
  } catch (e) {
    console.log(`${source.padEnd(10)} ${e.name}`);
  }
}

const line = '"Asha","Pune",411001';
const ports = [
  ['(?U)".*"', /".*?"/g],
  ['(?U)".*?"', /".*"/g],
  ['(?U)\\d+', /\d+?/g],
  ['(?U)\\d+?', /\d+/g],
];
console.log('');
for (const [ungreedy, port] of ports) {
  console.log(`${ungreedy.padEnd(10)} becomes ${String(port).padEnd(8)} ${line.match(port).join(' ')}`);
}

Output

(?U)".*"   SyntaxError
(?U:".*")  SyntaxError

(?U)".*"   becomes /".*?"/g "Asha" "Pune"
(?U)".*?"  becomes /".*"/g  "Asha","Pune"
(?U)\d+    becomes /\d+?/g  4 1 1 0 0 1
(?U)\d+?   becomes /\d+/g   411001

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ungreedy.mjs

Both spellings, (?U) and the group form (?U:...), are a SyntaxError in JavaScript. So a pattern written for ungreedy mode is ported by hand: put a question mark after every quantifier that has none, and take it away from every quantifier that has one. The four ported patterns print what the Go tab printed for the originals.

The same two characters mean three things

(?U) is ungreedy mode in Go, PCRE2 and PHP, an error in JavaScript and Python, and in Java the switch to Unicode character classes, Pattern.UNICODE_CHARACTER_CLASS, which shorthand classes covers. A pattern copied from one to another can stop compiling, or compile and match something else.

The mode costs readers something too: in a long pattern .* means one thing after the flag and the opposite before it. Use it knowingly, in code that only one engine runs, with a comment beside it; anywhere else, write the lazy quantifiers out.

Key idea

(?U) swaps lazy and greedy in Go, PCRE2 and PHP only; to port such a pattern, add a question mark to each quantifier that lacks one and remove it from each that has one.

Checkpoint

Check: Ungreedy mode and porting it

  1. Question 1 of 2 In Go, FindAllString with the pattern (?U)\d+ runs on 411001. What does it return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Six matches of one digit each: 4, 1, 1, 0, 0 and 1

    Go's syntax table describes U as a swap of x+ and x+?, so (?U)\d+ behaves like \d+? without the flag: six matches of one digit in 411001.

  2. Question 2 of 2 A PHP pattern /<.+>\d+?/U has to move to JavaScript and match the same text. Which JavaScript pattern does that?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: /<.+?>\d+/g

    Porting from ungreedy mode means adding a question mark to every quantifier that has none and removing it from every quantifier that has one. On <i>42 <b>7, the PHP pattern and /<.+?>\d+/g both find <i>42 and <b>7.

The exercise asks for one pattern that uses the whole lesson. A wiki page writes its links as [[Pune]] or [[Pune|the city]], and real pages hold two links on one line, a link that was never closed and empty brackets. The starter’s greedy pattern swallows several links at once; a lazy one does better but still runs over a broken link. The sample tests run in your browser, in JavaScript and in Python:

Regex Tester Paste rows of your own and compare the greedy, lazy and negated-class patterns on them; the tester matches with the JavaScript engine of the browser you are reading in.

Last, the self-check: five ideas, the weakest one first, with the reason behind every choice shown the moment you answer.

Practice that adapts to you

Practise this lesson

19 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 19 What does /<.+>/ match in <b>bold</b> text?

    Choose one answer.

    Show the answer: Question 1 of 19

    Answer: <b>bold</b>

    A greedy quantifier stops at the last place where the rest of the pattern can match, not at the first. Here .+ gives back the space and the word text, and the match ends at the > of </b>.

  2. Question 2 of 19 /\d+0/ runs on 1000. How many digits does \d+ hold in the match?

    Type a number.

    A hint

    The pattern needs one 0 after the digits that the quantifier holds.

    Show the answer: Question 2 of 19

    Answer: 3 digits

    \d+ first takes all four digits, and the 0 of the pattern fails at the end of the text. \d+ gives back the last 0 and the pattern's 0 matches it, so \d+ holds 100, three digits, and the match is the whole of 1000.

  3. Question 3 of 19 A log line reads level=warn msg="disk full" host="db1", and /msg=".*"/ returns msg="disk full" host="db1". What explains the overrun?

    Choose one answer.

    Show the answer: Question 3 of 19

    Answer: The greedy dot and star runs to the end of the line and gives back only until a quote can follow; the last quote of the line is the first one it reaches from the end

    Two fixes stop at the quote after full: msg=".*?" and msg="[^"]*". The second also says outright that the value holds no quote.

  4. Question 4 of 19 What does /a+?/ match in aaaa?

    Choose one answer.

    Show the answer: Question 4 of 19

    Answer: a

    A lazy quantifier keeps the counts of its greedy form and only prefers the minimum. The table of the lesson lists a+? taking a from aaaa.

  5. Question 5 of 19 What does "<b>".match(/<.+?/)[0] return in JavaScript?

    Choose one answer.

    Show the answer: Question 5 of 19

    Answer: <b

    This is the slip of the lesson's mistake callout: a lazy quantifier at the end of a pattern stops at its minimum. Writing the end, <.+?>, makes it extend to the first >.

  6. Question 6 of 19 /x+?y/ runs on xxxy. How many x characters does x+? hold in the match?

    Type a number.

    A hint

    After each x it takes, the lazy quantifier lets y try first.

    Show the answer: Question 6 of 19

    Answer: 3 characters

    x+? takes one x and y fails on the second x; it takes a second x and y fails on the third; it takes the third and y matches. The match is xxxy, with three x characters in x+?.

  7. Question 7 of 19 Which statement about a{2,3}? is true?

    Choose one answer.

    Show the answer: Question 7 of 19

    Answer: It takes two a characters when it can, and three only when the rest of the pattern needs them

    A lazy range tries its minimum first and adds one repetition at a time, up to its maximum, only when the rest of the pattern fails. Run on aaab, a{2,3}?b returns aaab and a{2}b returns aab.

  8. Question 8 of 19 Why does x.*?y on xxy return xxy and not the shorter xy?

    Choose one answer.

    Show the answer: Question 8 of 19

    Answer: A match that starts at position 0 succeeds, and the engine never tries a later start

    The leftmost start wins first. At position 0, .*? takes nothing, y fails on the second x, .*? takes that x and y matches: xxy. Position 1, where xy starts, is never tried.

  9. Question 9 of 19 What does /b.*?d/ find in abcbd?

    Choose one answer.

    Show the answer: Question 9 of 19

    Answer: bcbd, at position 1

    The engine tries start positions from the left: position 0 fails on the a, position 1 succeeds with bcbd, and the later bd is never tried.

  10. Question 10 of 19 Which pattern finds xy, and not xxy, in the text xxy?

    Choose one answer.

    Show the answer: Question 10 of 19

    Answer: x[^x]*y

    To change where a match starts, change what the pattern accepts. Laziness cannot do it; a class that refuses the opening character can.

  11. Question 11 of 19 In Go, a Regexp compiled from ".*?" is switched to leftmost-longest with Longest(). What does FindAllString return on "Goa","Agra"?

    Choose one answer.

    Show the answer: Question 11 of 19

    Answer: One match, "Goa","Agra"

    Lazy and greedy are preferences of a leftmost-first search. After Longest(), Go chooses the longest match from the leftmost start, as the lesson's Go tab shows for <.+?> on <a><b>.

  12. Question 12 of 19 Which pattern states that a tag ends at its first >?

    Choose one answer.

    Show the answer: Question 12 of 19

    Answer: <[^>]+>

    A negated class of the end character states the end outright: every character but >, then the >. On <a>b>, <[^>]+> finds <a>, while <[^<]+> takes all of <a>b>.

  13. Question 13 of 19 An HTML comment may hold a >, as in a <!-- x > y --> b. What does /<!--[^>]*-->/g find in that text?

    Choose one answer.

    Show the answer: Question 13 of 19

    Answer: Nothing: the class refuses the > inside the comment

    The rule of thumb holds for an end of one character. When the end is longer, such as --> or */, keep .*?, or use a parser, as the tip of the lesson says.

  14. Question 14 of 19 In the lesson's PCRE2 run on a quoted field of 1,000 characters, ".*?" needs a match limit of 1,003 and "[^"]*" needs 2. What makes the difference?

    Choose one answer.

    Show the answer: Question 14 of 19

    Answer: The lazy dot tries the closing quote after every character of the field, while the class runs over the field in one go

    A lazy dot pays one round for each character of the field, and a greedy one for each character after it, as the lesson's table shows (1,003 for the lazy pattern either way, 1,004 for the greedy one with text after the field). The class pays for neither.

  15. Question 15 of 19 A script pulls the values out of name='Asha' city='Pune' with '.*?'. Which rewrite says where each value ends?

    Choose one answer.

    Show the answer: Question 15 of 19

    Answer: '[^']*'

    The class has to refuse the very character that ends the value. On this line '[^']*' finds 'Asha' and 'Pune', while '[^"]*' and '.*' each return one match across both values.

  16. Question 16 of 19 What does (?U) at the start of a Go or PCRE2 pattern do?

    Choose one answer.

    Show the answer: Question 16 of 19

    Answer: It swaps greedy and lazy: plain quantifiers become lazy and those with a question mark greedy

    In Go, PCRE2 and PHP, (?U) inverts the default: ".*" becomes lazy and ".*?" greedy, as every tab of the ungreedy example prints.

  17. Question 17 of 19 What does new RegExp("(?U)a+") do in JavaScript?

    Choose one answer.

    Show the answer: Question 17 of 19

    Answer: It throws a SyntaxError

    Both (?U) and (?U:...) are a SyntaxError in JavaScript. A pattern written for ungreedy mode is ported by swapping its quantifiers by hand.

  18. Question 18 of 19 What does preg_match_all('/\d+?/U', '411001', $m) put in $m[0] in PHP?

    Choose one answer.

    Show the answer: Question 18 of 19

    Answer: One match, 411001

    PHP's U modifier sets PCRE2's ungreedy option: \d+ is lazy and \d+? greedy. The PHP tab of the ungreedy example prints 411001 for /\d+?/U.

  19. Question 19 of 19 A Go pattern (?U)<.+> must run in Python's re and find the same tags. What do you write?

    Choose one answer.

    Show the answer: Question 19 of 19

    Answer: <.+?>

    Porting from ungreedy mode is a swap: every quantifier without a question mark gets one, and every quantifier with one loses it. On <a><b>, Python's <.+?> finds <a> and <b>.

Interview questions

Warm-up: what changes when a quantifier is made lazy, and does a lazy pattern return the shortest match?

A lazy quantifier keeps the same repetition counts and changes only the order of the tries. A greedy * takes as many repetitions as it can and gives them back one at a time when the rest of the pattern fails; *? takes as few as it can and adds one at a time. So ".*" on "Asha","Pune",411001 returns one match that runs to the last quote, while ".*?" returns "Asha" and "Pune". Lazy does not mean shortest, though: the engine still keeps the first start position where the pattern matches, so x.*?y on xxy returns all three characters, not xy. A lazy quantifier can also run past the delimiter you meant when the rest of the pattern fails there, so when a field ends at one character, a negated class such as [^"]* states that limit directly.

Two harder questions follow: config blocks that run into each other, and a parser that leaves PHP’s ungreedy mode behind. Jot down the first thing you would check in each, and only then open it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A deploy script reads service blocks from a config file with re.findall(r"BEGIN(.*?)END", text, re.S). After one edit it sets up one service too few, and another service gets a host it never had. One block lost its END line. What did the pattern do, and how would you read the blocks instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A PHP log parser compiles every pattern with the U modifier, and a few patterns also use \d++, a{2,3}? or a (?-U) part. The parser moves to a Python worker, whose re has no ungreedy mode. How do you port the patterns so the worker returns exactly the matches PHP returned, and how do you prove it?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why does ".*" return one long match on the row "Asha","Pune",411001?

    The star is greedy: it takes everything and gives characters back from the end only as far as the closing quote needs, so the match runs to the last quote it can reach and holds both fields.

  2. What does the lazy pattern ".*?" return on the same row, and why?

    "Asha" and then "Pune". A lazy quantifier tries the rest of the pattern before each extra repetition, so it stops at the first closing quote that lets the whole pattern match.

  3. Does a lazy quantifier return the shortest possible match?

    No. Laziness decides how much a match takes, never where it starts, and the leftmost start wins first: x.*?y on xxy returns xxy, not xy.

  4. Why does /".*?/g return lone quotes instead of whole fields?

    Nothing follows .*? in the pattern, so it is content with zero characters. Say where the field ends, /".*?"/g, and the closing quote makes it extend.

  5. Why is "[^"]*" often a better field pattern than ".*?"?

    The class cannot run past the closing quote or cross a line break, so it stays right where the lazy dot goes wrong. On a 1,000-character field PCRE2 needs a match limit of 2 for it against 1,003 for the lazy dot.

  6. What does (?U) do, and how do you port a pattern written for it?

    In Go, PCRE2 and PHP it swaps greedy and lazy. It does not compile in JavaScript or Python and means Unicode classes in Java, so add a question mark to each quantifier without one and remove it from each that has one.

Key takeaways

  • A greedy quantifier takes every repetition it can and gives them back one at a time, so ".*" on a row of quoted fields gives back eight characters and still runs to the last quote: one match for two fields.
  • A lazy quantifier, a quantifier with a question mark after it, tries the rest of the pattern before each extra repetition, so ".*?" stops at the first closing quote that lets the pattern match.
  • Laziness never moves the start of a match: the leftmost start wins first, so x.*?y on xxy returns xxy, and under Go’s leftmost-longest Longest() a lazy quantifier makes no difference.
  • A negated class such as "[^"]*" cannot run past the closing quote or stop at a line break, so it finds the right fields where .*? does not, and PCRE2 needs a match limit of 2 for it against 1,003 for the lazy dot on a field of 1,000 characters.
  • (?U) swaps greedy and lazy in Go, PCRE2 and PHP, does not compile in JavaScript or Python and means Unicode classes in Java, so a pattern written for it is ported by swapping each quantifier by hand.

Terms in this lesson

greedy quantifier
A quantifier in its usual form, such as * or {2,5}, which repeats its atom as many times as it can and gives repetitions back one at a time only when the rest of the pattern cannot match otherwise.
lazy quantifier
A quantifier followed by a question mark, such as *? or {2,5}?, which repeats its atom as few times as it can and adds one repetition at a time only when the rest of the pattern cannot match otherwise. Also called non-greedy or reluctant.
ungreedy mode
An option of PCRE2, PHP (the U modifier) and Go, also set with (?U) in a pattern, that swaps the two forms: a quantifier without a question mark is lazy and one with it is greedy. JavaScript and Python have no such mode.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.