Regular Expressions Module 8 – Flavours compared and regex in each language
Regex in PCRE2, PHP and Perl
How pcre2grep and pcre2test, PHP preg functions and Perl share one regex dialect but differ in delimiters, modifiers, errors and replacements.
What you will learn
- Compare how pcre2test, PHP and Perl write a pattern, set its options and build a replacement
- Write a PHP pattern string with a delimiter and modifiers that suit the text, u included
- Diagnose a false from a preg function by telling it from 0 and reading the warning or the run error
- Rewrite a replacement that changes case or names a group for PCRE2, PHP and Perl
- Explain what a match limit stops and measure it with pcre2test
Before you start
What you’ll learn · Before you start
What you will learn
- Compare how pcre2test, PHP and Perl write a pattern, set its options and build a replacement
- Write a PHP pattern string with a delimiter and modifiers that suit the text, u included
- Diagnose a false from a preg function by telling it from 0 and reading the warning or the run error
- Rewrite a replacement that changes case or names a group for PCRE2, PHP and Perl
- Explain what a match limit stops and measure it with pcre2test
Before you start
On this page
PHP 8.5.11 runs its patterns through PCRE2 10.44, the pcre2test and pcre2grep programs on the same Mac are PCRE2
10.49, and Perl runs an engine of its own that PCRE2 imitates. Three front doors lead to nearly one
flavour: A particular regular-expression syntax and feature set, such as ECMAScript, Python re, PCRE2 or RE2; a pattern is written for one of them. More in “What a regular expression is and where it runs”, and each has its own way to write a pattern, to set its options and to build a
replacement: a missing modifier makes \d skip the Devanagari digit ७, and one stray character turns a call into false.
This lesson goes through the doors in turn: PHP delimiters and modifiers, how to read a failed preg_* call, the
replacements that only some doors can build, and the two shell programs that show what the library is doing. By the end
you can write a PHP pattern with the right delimiter and letters, diagnose a false, rewrite a replacement for each
door, and read a match limit.
One dialect, three front doors
The previous lesson compared flavours across many languages; this one stays in one family and looks at its doors. Start with what is installed. The run below prints the version of PHP and the version of the PCRE2 library inside it:
<?php
// Two version numbers: the PHP that runs the script and the PCRE2 library inside it.
// PCRE_VERSION also carries a release date after the number; only the number is printed.
echo "PHP_VERSION ", PHP_VERSION, "\n";
echo "PCRE2 library ", explode(' ', PCRE_VERSION)[0], "\n"; Output
PHP_VERSION 8.5.11 PCRE2 library 10.44
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php version.php
The library inside PHP 8.5.11 is 10.44, and the first line of every pcre2test transcript later in this lesson says
10.49. They are two releases of one library, so a feature added in between exists at one door and not at the other. The
PHP manual calls the dialect compatible with Perl’s, and the pcre2pattern manual says PCRE2 tries to match Perl’s syntax
and semantics as closely as it can. Perl itself is the odd door out: it keeps its own engine, so a pattern that works
there is a close relative of one that works in PHP, not the same program.
The figure follows a pattern through the four stages PCRE2 gives every user and marks where each door takes hold of it. Step through it before reading the table:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Four stages. PCRE2 turns the pattern text into a compiled form once, runs it against a subject, and can then build a replacement from the match. Every tool and language below reaches these stages in its own way.
- The shell tools. pcre2test reads a line such as /a+b/i, so the modifiers after the second slash are compile options. Modifiers after \= at the end of a subject line apply only to that match: find_limits and match_limit live there.
- PHP. A PHP pattern is a string whose first character is the delimiter; the letters after the closing delimiter, such as i or u, are the compile options. PHP 8.5.11 carries PCRE2 10.44, older than the 10.49 of pcre2test here.
- Perl. Perl does not call the PCRE2 library: its engine is its own, and PCRE2 imitates its syntax. A Perl replacement is an interpolated string, so \u, \U and \E change case there while the substitution is built.
- Where they part. The pattern language is shared; the surroundings are not. A delimiter, a modifier letter and the rules of a replacement string belong to the front door, which is why a pattern copied between doors needs a second look.
| Door | The pattern is written as | Options go | The replacement is |
|---|---|---|---|
pcre2test |
a line /pattern/modifiers |
after the closing slash; per subject after \= |
replace= text, read fully with substitute_extended |
| PHP | a string '/pattern/modifiers' |
letters after the closing delimiter | a string of \n, $n and ${n}, or a callback |
| Perl | s/pattern/replacement/flags in code |
flags after the last delimiter | a string interpolated like a double-quoted one |
Key idea
The pattern language is shared, but the frame around it is not: where options go and what a replacement may contain belong to the door you came in by.
Checkpoint
Check: Three front doors to one dialect
A PHP pattern is a string: delimiter first, modifiers last
A PHP pattern is an ordinary string, not a literal the language parses, so PHP finds the pattern’s edges from its first
character. That character is the pattern delimiter: The character that opens and closes a pattern written as a string, as in PHP's preg functions. It is the string's first character and must end the body; modifier letters follow the closing one.: the manual allows any single-byte
character that is not alphanumeric, not a backslash, not NUL and not white space, and the same character must end the
body. The bracket pairs (), {}, [] and <> work too, with the closing bracket ending the body. Three spellings of
one path test follow, and a fourth string whose first character was meant as an anchor:
<?php
// The first character of a PHP pattern string is its delimiter. Three spellings of the same path test,
// and one string whose first character was meant as an anchor.
$path = '/usr/local/share';
$patterns = [
'/^\/usr\/local\//', // slash delimiter: every slash in the body is escaped
'#^/usr/local/#', // hash delimiter: nothing to escape
'{^/usr/local/}', // bracket delimiters: the pair { }
'^/usr/local/', // no delimiter: PHP takes ^ as one and looks for a second ^
];
foreach ($patterns as $pattern) {
$result = @preg_match($pattern, $path);
printf("%-18s %s\n", $pattern, $result === false ? 'false: ' . error_get_last()['message'] : "returned $result");
} Output
/^\/usr\/local\// returned 1
#^/usr/local/# returned 1
{^/usr/local/} returned 1
^/usr/local/ false: preg_match(): No ending delimiter '^' found
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php delimiters.php
The slash version needs a backslash before every slash of the path; the hash and the braces need none. The last string
has no closing ^, so PHP warns that it found no ending delimiter and the call returns false. Which delimiter to pick
is a readability question only, since none of them changes what the body matches. Read a whole pattern from left to
right in the next figure:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The first character. In #^[1-9]\d{5}$#u the first character is #. PHP does not match it: it takes it as the delimiter and looks for the same character again further on. Any single-byte character that is not a letter, a digit, a backslash or white space can serve.
- The body. Between the delimiters, ^[1-9]\d{5}$ is a PCRE2 pattern: a first digit from 1 to 9, then five digits, from the start of the text to its end. PHP hands it over unchanged.
- The closing delimiter. The second # ends the body. A # inside the body would have to be written \# to be matched as text, which is why a pattern full of slashes is easier to read with a different delimiter.
- The modifier letters. Everything after the closing delimiter is a modifier. Here u means UTF-8: the pattern and the text are read as code points, and \d follows Unicode. A letter PHP does not know is a warning and a false result.
Everything after the closing delimiter is a pattern modifier: A letter written after the closing delimiter of a pattern that sets an option for the whole pattern, such as i for case-insensitive matching, m for line anchors or u for UTF-8 and Unicode classes., one letter each. The eight that
change results most often are in the table, and the run after it tries seven of them with and without the letter (u
has a run of its own further down):
| Letter | What it changes |
|---|---|
i |
letters match both cases |
m |
^ and $ also match at every line break |
s |
the dot matches a line break too |
x |
white space and # comments in the pattern are ignored |
u |
pattern and text are UTF-8, and \d, \w and \s follow Unicode |
U |
greedy and lazy swap meaning |
A |
the match must start at the first position |
D |
$ matches only at the very end, not before a final newline |
S is still accepted but has done nothing since PHP 7.3, and n (PHP 8.2) makes plain groups non-capturing.
<?php
// One modifier at a time: the same subject with and without the letter after the closing delimiter.
$rows = [
['i', '/error/', '/error/i', 'ERROR: disk full'],
['m', '/^b$/', '/^b$/m', "a\nb\nc"],
['s', '/a.b/', '/a.b/s', "a\nb"],
['x', '/\d{3} \d{3}/', '/\d{3} \d{3}/x', '411001'],
['U', '/<.+>/', '/<.+>/U', '<b>bold</b>'],
['A', '/b/', '/b/A', 'ab'],
['D', '/^a$/', '/^a$/D', "a\n"],
];
foreach ($rows as [$letter, $without, $with, $subject]) {
echo $letter, ' on ', json_encode($subject, JSON_UNESCAPED_SLASHES), "\n";
foreach ([$without, $with] as $pattern) {
$found = preg_match($pattern, $subject, $m) ? json_encode($m[0], JSON_UNESCAPED_SLASHES) : 'no match';
printf(" %-16s %s\n", $pattern, $found);
}
} Output
i on "ERROR: disk full"
/error/ no match
/error/i "ERROR"
m on "a\nb\nc"
/^b$/ no match
/^b$/m "b"
s on "a\nb"
/a.b/ no match
/a.b/s "a\nb"
x on "411001"
/\d{3} \d{3}/ no match
/\d{3} \d{3}/x "411001"
U on "<b>bold</b>"
/<.+>/ "<b>bold</b>"
/<.+>/U "<b>"
A on "ab"
/b/ "b"
/b/A no match
D on "a\n"
/^a$/ "a"
/^a$/D no match
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php modifiers.php
Two rows deserve a second look. With U, which 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. More in “Greedy and lazy quantifiers”, /<.+>/ stops at the first > and returns <b>, the reverse of what a
reader of the pattern expects. And D rejects "a\n" where plain /^a$/ accepts it, the
trailing-newline trap: A validation bug: a pattern such as ^abc$ accepts abc followed by a line feed in engines whose $ also holds before a final newline, so the checked value still carries the extra character. More in “Anchors: start, end and multiline mode”: a value that came from a form or a file keeps its final
newline, and a bare $ lets it through.
Validating with a bare dollar sign
Wrong: /^\d{6}$/ for a postal code, which accepts "411001\n". Right: /^\d{6}$/D, which ends the match at the
true end of the text. The exercise at the end of the lesson turns this into a test.
The u modifier does two jobs, and the next run shows both. It makes \d take ७, as the
shorthand classes lesson explains, and it makes PHP check that the text is valid UTF-8:
a stray byte makes the call fail instead of matching bytes.
<?php
// The u modifier in PHP: UTF-8 text, Unicode properties for \d, and a check that the subject is valid UTF-8.
$text = "\u{96D}9"; // the Devanagari digit seven, then the ASCII digit 9
foreach (['/\d/', '/\d/u'] as $pattern) {
preg_match_all($pattern, $text, $found);
printf("%-6s %s\n", $pattern, json_encode($found[0], JSON_UNESCAPED_UNICODE));
}
// A subject that is not valid UTF-8 (one stray byte): the u pattern refuses to run.
$bytes = "ab\xFF";
foreach (['/./', '/./u'] as $pattern) {
$count = preg_match_all($pattern, $bytes);
printf("%-6s %s\n", $pattern, $count === false ? 'false: ' . preg_last_error_msg() : "$count matches");
} Output
/\d/ ["9"] /\d/u ["७","9"] /./ 3 matches /./u false: Malformed UTF-8 characters, possibly incorrectly encoded
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php u_flag.php
Checkpoint
Check: Delimiters and modifiers in PHP
A failed preg call returns false, not zero
preg_match has three answers: 1 for a match, 0 for none, and false when the call fails. Failure has two causes,
and they report differently. A pattern that does not compile raises a PHP warning with the library’s own description,
and a run that goes wrong afterwards, such as bad UTF-8 or too much work, is named by preg_last_error_msg(). The run
shows a compile failure and why the return value must be tested strictly:
<?php
// A pattern that does not compile: preg_match() returns false, PHP raises a warning,
// and preg_last_error_msg() has only a general word for it.
$bad = @preg_match('/(/', 'x');
echo "warning: ", error_get_last()['message'], "\n";
echo "last error: ", preg_last_error(), ' = ', preg_last_error_msg(), "\n";
// A good pattern that finds nothing returns 0, which is not false.
$none = preg_match('/zz/', 'x');
var_dump($bad, $none);
var_dump($bad == $none); // loose comparison: false == 0
var_dump($bad === $none); // strict comparison tells them apart
foreach ([$bad, $none, preg_match('/x/', 'x')] as $result) {
echo match (true) {
$result === false => "error: the pattern or the run failed\n",
$result === 0 => "no match\n",
default => "match\n",
};
} Output
warning: preg_match(): Compilation failed: missing closing parenthesis at offset 1 last error: 1 = Internal error bool(false) int(0) bool(true) bool(false) error: the pattern or the run failed no match match
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php error.php
The warning says missing closing parenthesis at offset 1, but preg_last_error_msg() says only Internal error,
because the pattern never ran and so there is no run error to describe. The detail lives in the warning, which the @
here hid from the screen but error_get_last() still holds. The loose comparison false == 0 is true, so a check
written as if (!$result) treats a broken pattern as an ordinary non-match. Step through the decision once:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- One call, one answer. preg_match returns 1 for a match, 0 for no match and false when something went wrong. The three answers come back through the same return value, so the test decides how they are told apart.
- A match and no match. A pattern that finds nothing is a normal result, 0, and the subject is fine. A pattern that finds a match gives 1. Neither needs a second look.
- Why == is the wrong test. In PHP, false == 0 is true. A check written with == or with ! lumps an error together with no match. Only === false answers the question: did the call fail?
- A failed call. After false, two places say more. A compile error is a PHP warning that names the problem and its offset. A run-time failure, such as the backtrack limit, is read with preg_last_error_msg() straight after the call.
A second kind of false comes from the run itself. PHP stops a match after pcre.backtrack_limit steps, set to
1,000,000 here. A pattern with a repeat inside a repeat can need far more than that on text that almost matches, and the
call gives up with false. The limit is a safety net, not a setting to tune:
<?php
// The backtrack limit is a safety net: when a run exceeds it, preg_match() returns false
// and preg_last_error_msg() says why. The fix is the pattern, not a bigger limit.
echo "pcre.backtrack_limit = ", ini_get('pcre.backtrack_limit'), "\n";
$subject = str_repeat('a', 26) . '!';
foreach (['/^(a+)+$/' => 'nested repeat', '/^a+$/' => 'one repeat'] as $pattern => $label) {
$result = preg_match($pattern, $subject);
printf("%-12s %-14s %s\n", $pattern, $label, $result === false ? 'false: ' . preg_last_error_msg() : "returned $result");
} Output
pcre.backtrack_limit = 1000000 /^(a+)+$/ nested repeat false: Backtrack limit exhausted /^a+$/ one repeat returned 0
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php limit.php
The nested (a+)+ runs into the limit on 26 letters a followed by !, while the single repeat a+ returns 0 straight
away. Both accept exactly the same strings, so the fix is the pattern: remove the repeat inside a repeat, and do not
raise the limit. Why such patterns blow up, and how to test for them, belongs to the performance and security module
later in this track.
Key idea
Test every preg result with === false, read a compile problem in the warning, read a run problem in
preg_last_error_msg(), and treat a limit error as a sign that the pattern needs fixing.
Checkpoint
Check: Reading false from a preg call
Replacements: what only some doors can do
Take one job: turn hello world into World HELLO!, with the second word first and capitalised and the first word in
capitals. PHP’s preg_replace reads only three things in a replacement: \n, $n and ${n}, where n is a group
number. A group’s name is not one of them, and a digit after $1 is read as part of the number:
<?php
// preg_replace() reads \n, $n and ${n} in a replacement. Nothing else: a group name is not a reference.
echo preg_replace('/(?<a>\w+)/', '${a}!', 'hi'), "\n";
echo preg_replace('/(\w+)/', '$1!', 'hi'), "\n";
echo preg_replace('/(\w+)/', '${1}!', 'hi'), "\n";
// A digit after $1 is read as part of the group number: $10 is group 10, which does not exist.
echo '[', preg_replace('/(a)/', '$10', 'a'), "]\n";
echo '[', preg_replace('/(a)/', '${1}0', 'a'), "]\n";
// Names and case changes need a function: preg_replace_callback() hands over the groups by name.
echo preg_replace_callback('/(?<w>\w+)/', fn($m) => strtoupper($m['w']) . '!', 'hello world'), "\n"; Output
${a}!
hi!
hi!
[]
[a0]
HELLO! WORLD!
Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php named_replace.php
The first line shows the name ${a} copied as it stands. $10 means group 10, which does not exist, so it inserts
nothing; ${1}0 is group 1 and then a zero. Anything beyond numbered groups goes to preg_replace_callback, which
hands a function the groups by name, as the last line does to upper-case both words.
Naming a group in preg_replace
Wrong: preg_replace('/(?<a>\w+)/', '${a}!', 'hi'), which returns ${a}!. Right: '${1}!' for a number, or
preg_replace_callback with $m['a'] for a name.
PCRE2 itself can do the whole job. In pcre2test, the replace= modifier gives the replacement text, and
substitute_extended makes the library read it fully. Then case conversion escape: A backslash escape in a replacement that changes the case of inserted text: \u and \l change the next letter, \U and \L change everything up to \E. PCRE2 reads them with substitute_extended, and Perl always. work: \u and \l change the next letter, \U and \L change everything up to \E, and ${name}
reads a named group. The first block below is the same replacement without the option:
PCRE2 10.49 · pcre2-replace/pcre2_replace.py
"""Case-conversion escapes in a PCRE2 replacement, through pcre2test.
The replace= modifier gives the replacement string. Without substitute_extended, the library copies the backslash
sequences as they stand; with it, \\u and \\l change the next letter, \\U and \\L change everything up to \\E, and
${name} reads a named group. The line after the pattern shows the subject; " 1: " is the number of replacements
followed by the new text.
"""
from pcre2_session import run
run(r'''
# the replacement as plain text: no modifier, no case change
/(\w+)\s(\w+)/replace=\u$2 \U$1\E!
hello world
# the same replacement with substitute_extended
/(\w+)\s(\w+)/replace=\u$2 \U$1\E!,substitute_extended
hello world
# a named group in the replacement, and \L lowering a shouted word
/(?<city>[A-Z]+), (?<pin>\d{6})/replace=${pin} \L${city}\E,substitute_extended
PUNE, 411001
''') Output
$ pcre2test -q patterns.txt (PCRE2 10.49)
# the replacement as plain text: no modifier, no case change
/(\w+)\s(\w+)/replace=\u$2 \U$1\E!
hello world
1: \uworld \Uhello\E!
# the same replacement with substitute_extended
/(\w+)\s(\w+)/replace=\u$2 \U$1\E!,substitute_extended
hello world
1: World HELLO!
# a named group in the replacement, and \L lowering a shouted word
/(?<city>[A-Z]+), (?<pin>\d{6})/replace=${pin} \L${city}\E,substitute_extended
PUNE, 411001
1: 411001 pune
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pcre2_replace.py
pcre2-replace/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] Without the option the library copies the backslashes: \uworld \Uhello\E!. With it the line reads World HELLO!, and
the third block swaps two named groups and lowers one. The manual adds that a \u straight before a \L makes a title
case, so a group holding heLLo comes out as Hello. The case escapes act on everything inserted after them,
literal text included.
Perl builds the same result without any option. The replacement part of s/// is an interpolated string, so the case
escapes of perlop work in it, and $+{name} reads a named group (perlre). The /r modifier returns the changed copy and
leaves the original string alone. In Perl, s/(\w+)\s(\w+)/\u$2 \U$1\E!/r on hello world gives World HELLO!, and
s/(\w+)/\u$1/g gives Hello World. \u is documented as title-casing the
next character, which for a lone letter is the capital.
Key idea
PHP’s preg_replace takes numbers only; PCRE2 with substitute_extended and Perl read case escapes and names in the
replacement itself, and in PHP a callback does that work.
Checkpoint
Check: What a replacement can hold
pcre2grep and pcre2test show what the library does
pcre2grep filters files the way grep does, with the PCRE2 dialect. By default it prints each line that matches. With
-o it prints only the matched part, with -o3 only what capturing group 3 captured, and it prints nothing for a line
that does not match. pcre2test is the workbench behind it: it reads patterns and subjects from a script and prints
the match and every group for each subject. The run below models a pcre2grep search of four synthetic access-log
lines, keeping only failed requests (status 4xx or 5xx), with private addresses:
PCRE2 10.49 · pcre2grep-logs/pcre2grep_demo.py
"""What pcre2grep prints for failed requests, modelled with pcre2test on four synthetic access-log lines.
pcre2grep -o shows only the part of a line that matched, -o1 only what group 1 captured, and a line that does not match
prints nothing. pcre2test prints the same pieces numbered: " 0:" is the whole match (what -o shows), " 1:" and on are
the groups (-o1, -o2 ...), and "No match" is a line grep would skip. A subject modifier get=name, written after \\=,
prints one group by its name. Addresses are private ones.
"""
from pcre2_session import run
run(r'''
# lines of the form: address method path status; keep the 4xx and 5xx ones
/^(?<ip>\d+\.\d+\.\d+\.\d+) (?<method>[A-Z]+) (?<path>\S+) (?<status>[45]\d\d)$/
10.0.4.17 GET /learn/regex/ 200
10.0.4.17 GET /learn/missing 404
192.168.1.9 POST /api/login 500
192.168.1.9 GET /feed 503
# the same pattern, asking for two of the named groups by name
/^(?<ip>\d+\.\d+\.\d+\.\d+) (?<method>[A-Z]+) (?<path>\S+) (?<status>[45]\d\d)$/
192.168.1.9 POST /api/login 500\=get=path,get=status
''') Output
$ pcre2test -q patterns.txt (PCRE2 10.49)
# lines of the form: address method path status; keep the 4xx and 5xx ones
/^(?<ip>\d+\.\d+\.\d+\.\d+) (?<method>[A-Z]+) (?<path>\S+) (?<status>[45]\d\d)$/
10.0.4.17 GET /learn/regex/ 200
No match
10.0.4.17 GET /learn/missing 404
0: 10.0.4.17 GET /learn/missing 404
1: 10.0.4.17
2: GET
3: /learn/missing
4: 404
192.168.1.9 POST /api/login 500
0: 192.168.1.9 POST /api/login 500
1: 192.168.1.9
2: POST
3: /api/login
4: 500
192.168.1.9 GET /feed 503
0: 192.168.1.9 GET /feed 503
1: 192.168.1.9
2: GET
3: /feed
4: 503
# the same pattern, asking for two of the named groups by name
/^(?<ip>\d+\.\d+\.\d+\.\d+) (?<method>[A-Z]+) (?<path>\S+) (?<status>[45]\d\d)$/
192.168.1.9 POST /api/login 500\=get=path,get=status
0: 192.168.1.9 POST /api/login 500
1: 192.168.1.9
2: POST
3: /api/login
4: 500
G /api/login (10) path (group 3)
G 500 (3) status (group 4)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pcre2grep_demo.py
pcre2grep-logs/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 first log line has status 200, so pcre2test prints No match, where pcre2grep would stay silent. For the other
three, 0: is the whole match, which is what -o prints, and 1: to 4: are the four groups that -o1 to -o4
pick. A subject modifier after \=, here get=path,get=status, prints groups by their names.
The same program measures how much work a pattern costs, which is the match limit: A cap on the steps of the matching loop of PCRE2 that turns a runaway match into an error instead of an endless run. pcre2test reports error -47 when it is reached, and PHP returns false.. PCRE2
counts the steps of its main matching loop, and the limit stops a match when the count is reached. find_limits asks
pcre2test for the smallest limit that still lets a match finish, and match_limit=N sets one:
PCRE2 10.49 · pcre2-find-limits/pcre2_find_limits.py
"""The match limit of PCRE2, measured and enforced through pcre2test.
find_limits and match_limit=N are modifiers of a subject line, written after \\= at its end. find_limits makes the
library search for the smallest limits that still let the match finish; match_limit=N stops a match after N steps of
its main loop and reports error -47. The pattern is the nested repeat of the lesson; every subject ends in a character
that makes each attempt fail. The first three subjects have 14, 18 and 22 letters a; the fourth has the 26 letters of
the PHP run.
"""
from pcre2_session import run
run(r'''
/^(a+)+$/
aaaaaaaaaaaaaa!\=find_limits_noheap
aaaaaaaaaaaaaaaaaa!\=find_limits_noheap
aaaaaaaaaaaaaaaaaaaaaa!\=find_limits_noheap
aaaaaaaaaaaaaaaaaaaaaaaaaa!\=match_limit=1000
# one repeat: the same subject fails at once
/^a+$/
aaaaaaaaaaaaaaaaaaaaaaaaaa!\=find_limits_noheap
''') Output
$ pcre2test -q patterns.txt (PCRE2 10.49)
/^(a+)+$/
aaaaaaaaaaaaaa!\=find_limits_noheap
Minimum match limit = 40960
Minimum depth limit = 31
No match
aaaaaaaaaaaaaaaaaa!\=find_limits_noheap
Minimum match limit = 655360
Minimum depth limit = 39
No match
aaaaaaaaaaaaaaaaaaaaaa!\=find_limits_noheap
Minimum match limit = 10485760
Minimum depth limit = 47
No match
aaaaaaaaaaaaaaaaaaaaaaaaaa!\=match_limit=1000
Failed: error -47: match limit exceeded
# one repeat: the same subject fails at once
/^a+$/
aaaaaaaaaaaaaaaaaaaaaaaaaa!\=find_limits_noheap
Minimum match limit = 2
Minimum depth limit = 2
No match
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pcre2_find_limits.py
pcre2-find-limits/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] For ^(a+)+$ the minimum is 40,960 steps with 14 letters, 655,360 with 18 and 10,485,760 with 22: four more letters
multiply the work by 16. With a limit of 1000 the 26-letter subject of the PHP run ends as error -47, a failure that
is not the same as No match. The single repeat ^a+$ needs 2. pcre2grep takes --match-limit for the same purpose, and PHP’s
pcre.backtrack_limit is the same kind of guard: they turn a very long run into an error, and the pattern still
needs fixing.
Check the growth yourself
Copy the script to a terminal with pcre2test 10.49 and try 16 and 20 letters a. Each four letters should multiply
the minimum limit by 16, as the three recorded lines do.
Checkpoint
Check: pcre2grep, pcre2test and the match limit
Choosing a front door for the job
The choice depends on where the pattern has to run and on what the replacement must hold. Answer the two questions in the table and watch which doors are ruled out, and why:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Front door | See what a pattern does | Change case in a replacement | Name a group in a replacement | Say why a pattern failed |
|---|---|---|---|---|
| pcre2test and pcre2grep You write: a command on files or a test script | Good pcre2test prints every group; pcre2grep -o prints the match | Good substitute_extended gives \u, \l, \U, \L and \E | Good ${name} works with substitute_extended | Good error 114 at offset 1: missing closing parenthesis |
| PHP preg functions You write: a call inside a PHP program | Mixed you print the match array yourself in a script | Poor preg_replace has no case escapes: use a callback | Poor only \n, $n and ${n} are read: names need a callback | Mixed false, and the detail is in a warning, not in the error message |
| Perl s/// and m// You write: an operator in a Perl script | Mixed you print the captures yourself in a script | Good \u, \l, \U, \L and \E work in the replacement | Good $+{name} is read in the replacement | Good the program stops with the spot marked <-- HERE |
How the answers decide
- “Where does the pattern have to run?” Inside a PHP application: rules out pcre2test and pcre2grep. A PHP application calls the preg functions in its own process; pcre2grep and pcre2test are programs to start, which fit a pipeline, not a request.
- “Where does the pattern have to run?” Inside a PHP application: adds a caveat to Perl s/// and m//. Perl runs an engine of its own, so a pattern tested there is a second dialect to check before PHP runs it.
- “Where does the pattern have to run?” On files or a stream, from a shell: adds a caveat to PHP preg functions. A PHP script must open and loop over the files itself, where pcre2grep takes the files as arguments.
- “Where does the pattern have to run?” In a script that rewrites text: rules out pcre2test and pcre2grep. pcre2grep only reads files and pcre2test only tests patterns: neither rewrites a text for you.
- “Does the replacement change case or use group names?” Yes, one or both: rules out PHP preg functions. preg_replace reads \n, $n and ${n} only; case changes and names need preg_replace_callback and a function.
- “Does the replacement change case or use group names?” No, only numbered groups: adds a caveat to pcre2test and pcre2grep. With only numbered groups every door works; the shell tools give the most detail while you are still checking the pattern.
Each answer rules out a door only for the reason the table gives, and a ruled-out door can still be the right one for
a different job: the same person often keeps pcre2test open beside a PHP editor.
In short: use pcre2test and pcre2grep to see what a pattern does and to filter files, use PHP’s functions inside a
PHP program and keep to numbered groups or a callback in replacements, and use Perl for a rewrite that needs case
escapes or names in one string. Test the pattern in the engine that will run it, since Perl and PCRE2 are close and
not the same.
Checkpoint
Check: Choosing a front door
Practise: validators that explain a bad pattern
The exercise rebuilds the lesson’s habits in one small module: three field validators that anchor the whole text, take
only ASCII digits and letters and refuse a final newline, and a pattern compiler that reports a bad pattern with its
position instead of raising. It is written in Python, the language its tests run in, in your browser; the ideas carry over, and
re.fullmatch does the work of PHP’s anchors and D.
Exercise · Medium · Python
Field validators that explain a bad pattern
Write two functions in validators.py. The lesson's runs are PHP code; this exercise is written in Python, so the same ideas are written with Python's re, where the pattern is a plain string and re.fullmatch plays the part of PHP's D modifier and anchors.
check_field(kind, text) validates one field of a form and returns a dict. The three kinds are shapes this exercise defines (they copy no real postal, banking or telephone scheme):
"postal": six digits, the first one from 1 to 9. One space is allowed after the third digit, as in411 001. The value is the six digits without the space."branch": four letters followed by seven digits, such asABCD1234567. The letters may be upper or lower case. The value is the code in upper case."mobile": ten digits, the first one from 6 to 9. One space is allowed after the fifth digit, as in98765 43210. The value is the ten digits without the space.
A good field returns {"ok": True, "value": <the normalised value>}. A bad field returns {"ok": False, "error": "not a valid postal code"}, with postal code, branch code or mobile number in the message. An unknown kind returns {"ok": False, "error": "unknown kind: pan"} (with the kind that was passed).
Only the ASCII digits 0 to 9 and the ASCII letters count: the Devanagari digits ४११००१ are not a postal code. A text with a trailing newline, such as "411001\n", is not valid either, because the whole text must fit the shape.
compile_checked(pattern) compiles a pattern the user typed and never raises. It returns (compiled, None) when the pattern is good, and (None, message) when it is not, where the message reads pattern error at position 0: followed by the library's own description. The position is the one the re.error reports, the way a PHP program reads the offset in a compile warning.
check_field("postal", "411 001") # {"ok": True, "value": "411001"}
check_field("branch", "abcd1234567") # {"ok": True, "value": "ABCD1234567"}
compile_checked("(") # (None, "pattern error at position 0: missing ), unterminated subpattern")
The starter checks postal codes with re.match and $, which is where the sample tests show two bugs: a final newline slips through, and digits of other scripts are accepted. The other two kinds and the error report are yours.
Starter code · validators.py
import re
def check_field(kind, text):
"""Validates one form field and returns {"ok": True, "value": ...} or {"ok": False, "error": ...}."""
if kind == "postal":
# Bug: `$` also matches before a final newline, and `\d` takes digits of other scripts.
if re.match(r"^\d{6}$", text):
return {"ok": True, "value": text}
return {"ok": False, "error": "not a valid postal code"}
return {"ok": False, "error": "unknown kind: " + str(kind)}
def compile_checked(pattern):
"""Compiles a user's pattern and returns (compiled, None) or (None, "pattern error at position N: ...")."""
return re.compile(pattern), None The sample tests · test_validators.py
from validators import check_field, compile_checked
def test_postal_plain_and_spaced():
"""six digits, or three, one space and three; the value has no space"""
assert check_field("postal", "411001") == {"ok": True, "value": "411001"}
assert check_field("postal", "411 001") == {"ok": True, "value": "411001"}
def test_postal_shapes_that_fail():
"""a leading zero, a short code and two spaces are refused with the kind's message"""
for text in ("011001", "41100", "411 001", "4110011"):
assert check_field("postal", text) == {"ok": False, "error": "not a valid postal code"}
def test_postal_trailing_newline():
"""the whole text must fit the shape: a final newline is not allowed"""
assert check_field("postal", "411001\n")["ok"] is False
def test_postal_ascii_digits_only():
"""Devanagari digits are digits to Python's \\d but are not a postal code here"""
assert check_field("postal", "४११००१")["ok"] is False
def test_branch_code():
"""four letters and seven digits; lower case in, upper case out"""
assert check_field("branch", "ABCD1234567") == {"ok": True, "value": "ABCD1234567"}
assert check_field("branch", "abcd1234567") == {"ok": True, "value": "ABCD1234567"}
for text in ("ABCD123456", "ABC12345678", "ABCD12345678", "ABCD123456X", "ABCD1234567\n"):
assert check_field("branch", text) == {"ok": False, "error": "not a valid branch code"}
def test_mobile_number():
"""ten digits starting 6 to 9, with an optional space after the fifth"""
assert check_field("mobile", "9876543210") == {"ok": True, "value": "9876543210"}
assert check_field("mobile", "98765 43210") == {"ok": True, "value": "9876543210"}
for text in ("5876543210", "987654321", "98765 43210", "98765-43210"):
assert check_field("mobile", text) == {"ok": False, "error": "not a valid mobile number"}
def test_unknown_kind():
"""a kind the function does not know is reported, not raised"""
assert check_field("pan", "x") == {"ok": False, "error": "unknown kind: pan"}
def test_compile_checked_good_pattern():
"""a good pattern comes back compiled, with no message"""
compiled, error = compile_checked("[0-9]{6}")
assert error is None
assert compiled.fullmatch("411001")
def test_compile_checked_bad_pattern():
"""a bad pattern gives no compiled object and a message with the position"""
compiled, error = compile_checked("(")
assert compiled is None
assert error.startswith("pattern error at position 0: ")
compiled, error = compile_checked("ab)")
assert compiled is None
assert error.startswith("pattern error at position 2: ") A hint
Compile one pattern per kind and test the whole text with re.fullmatch, which cannot accept a trailing newline the way $ does. Write the digits as [0-9] instead of \d, so that Devanagari digits are refused, and capture the two halves of the postal and mobile numbers as groups so that the optional space drops out when you join them. For compile_checked, catch re.error and read its pos and msg.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Last, a round of questions on all six ideas. It opens on the one you handled least well and tells you why each answer is right or wrong the moment you pick it:
Practice that adapts to you
Practise this lesson
19 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: in PHP, which delimiter would you choose for a pattern that matches paths such as /learn/regex/, and where
do the modifier letters go?
Choose a delimiter that does not occur in the pattern. A path pattern is full of slashes, so / would need a backslash
before each one, as in /^\/learn\/regex\//. A hash avoids that: #^/learn/regex/#. Braces work as a pair too:
{^/learn/regex/}. The first character of the string is the delimiter, and the same character, or the closing bracket
of a pair, ends the body. Everything after it is read as modifier letters, so a case-insensitive version is
#^/learn/regex/#i. A delimiter that stays inside the body must be escaped, and a letter, a digit, a backslash or white
space is refused. When the delimiter comes from a variable, preg_quote($text, '#') escapes it for the body. The
delimiter never changes what the pattern matches, only how readable it is.
Two harder questions follow, one on a failed call that reports almost nothing and one on a replacement PHP cannot read. Decide what you would check first in each, then open it:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to staff
A PHP service calls @preg_match('/(/', 'x'). It returns false, and preg_last_error_msg() says Internal error. Explain what happened, where the real reason is, and the two habits that stop this bug from reaching production.
-
Fresher to staff
Write the PCRE2 replacement that turns hello world into World HELLO! with extended substitution. Say which modifier makes it work, what it prints without it, and why PHP's preg_replace cannot do the same.
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What do PHP, pcre2test and Perl share, and what differs between them?
PHP and the pcre2test and pcre2grep programs share PCRE2's pattern language in different releases (10.44 in PHP 8.5.11, 10.49 for the programs), and Perl runs its own engine that PCRE2 imitates. Options and replacements belong to each door.
-
Which delimiter suits a PHP pattern for paths such as /learn/regex/, and where do modifiers go?
A character the pattern does not contain, such as #^/learn/regex/#, or a pair of braces, so no slash needs a backslash. Modifier letters follow the closing delimiter, as in #^/learn/regex/#i.
-
Why does /^\d{6}$/ accept a postal code followed by a newline in PHP, and what fixes it?
PCRE2's $ also holds before a final newline. The D modifier, /^\d{6}$/D, makes $ hold only at the true end of the text.
-
What does the u modifier change for a PHP pattern?
PCRE2 reads the pattern and the text as UTF-8, and \d takes Unicode digits too, so without u a Devanagari digit such as ७ is skipped.
-
How should you test what preg_match returned?
With === false. It returns 1, 0 or false, and only false means the call failed: read a compile problem in the warning and a run problem in preg_last_error_msg().
-
What may a preg_replace replacement contain, and where do case escapes and names work?
Only \n, $n and ${n}. PCRE2 with substitute_extended and Perl also read case escapes and group names in the replacement; in PHP, preg_replace_callback does that work.
-
What does PCRE2's match limit do when a pattern runs away?
It counts matching steps and turns the runaway match into an error: false in PHP and error -47 in pcre2test. The cure is a pattern without a repeat inside a repeat.
Key takeaways
- PHP,
pcre2testandpcre2grepshare PCRE2’s pattern language, but they carry different releases of it (10.44 and 10.49 here), and Perl runs an engine of its own that PCRE2 only imitates. - A PHP pattern is a string whose first character is the delimiter, and the letters after the closing delimiter are
modifiers, such as
ufor UTF-8 and Unicode digits andDfor a$that ignores a final newline. preg_matchreturns1,0orfalse, so test it with=== false; read a compile problem in the warning and a run problem inpreg_last_error_msg().- A replacement for
preg_replacecan hold only\n,$nand${n}, while PCRE2 withsubstitute_extendedand Perl also read case escapes and group names, and PHP does the same work inpreg_replace_callback. - The match limit counts matching steps and turns a runaway pattern into an error,
falsein PHP anderror -47inpcre2test; the cure is a pattern without a repeat inside a repeat.
Terms in this lesson
- case conversion escape
- A backslash escape in a replacement that changes the case of inserted text: \u and \l change the next letter, \U and \L change everything up to \E. PCRE2 reads them with substitute_extended, and Perl always.
- match limit
- A cap on the steps of the matching loop of PCRE2 that turns a runaway match into an error instead of an endless run. pcre2test reports error -47 when it is reached, and PHP returns false.
- pattern delimiter
- The character that opens and closes a pattern written as a string, as in PHP's preg functions. It is the string's first character and must end the body; modifier letters follow the closing one.
- pattern modifier
- A letter written after the closing delimiter of a pattern that sets an option for the whole pattern, such as i for case-insensitive matching, m for line anchors or u for UTF-8 and Unicode classes.
References
- PHP Manual: Regular Expressions (Perl-Compatible) (The PHP Group)
- PHP Manual: Delimiters (The PHP Group)
- PHP Manual: Pattern Modifiers (The PHP Group)
- PHP Manual: preg_match, return values (The PHP Group)
- PHP Manual: preg_replace, replacement references (The PHP Group)
- PHP Manual: preg_last_error_msg (The PHP Group)
- PHP Manual: PCRE runtime configuration (pcre.backtrack_limit) (The PHP Group)
- pcre2pattern: PCRE2 regular expression syntax and semantics (PCRE2 Project)
- pcre2api: PCRE2_SUBSTITUTE_EXTENDED and case forcing in a replacement (PCRE2 Project)
- pcre2_substitute (PCRE2 Project)
- pcre2grep: options -o and --match-limit (PCRE2 Project)
- pcre2test: find_limits, match_limit, get and substitute_extended (PCRE2 Project)
- perlre: Perl regular expressions (Perl Documentation)
- perlop: quote-like operators, s///, the /r modifier and the case escapes (Perl Documentation)
- perlretut: Perl regular expressions tutorial (Perl Documentation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress