Flags and Modes
Changing how a pattern behaves: case-insensitive, multiline and dotall modes, verbose patterns with comments, Unicode mode, global and sticky matching, and inline flags.
What flags are
A pattern says what to look for. A regex flag (also called a modifier or option) says how to look: whether upper and lower case count as the same letter, whether the dot may cross a line break, whether to find one match or all of them. Flags are switches that apply to the whole pattern, or, as inline flags, to part of it. The pattern text stays the same; only its interpretation changes.
Every language has its own way to pass them. Python takes them as an
extra argument, combined with |. Each flag has a long
name and a one-letter alias, so re.IGNORECASE and
re.I are the same thing.
import re
log = """2026-03-02 09:14:07 portal ERROR login failed for INC-31877
2026-03-02 09:14:09 portal Error session timeout
2026-03-02 09:15:30 meter warning reading delayed"""
print(re.findall(r"error", log))
print(re.findall(r"error", log, re.IGNORECASE))
print(re.findall(r"^\S+ 09:14:\d\d", log, re.I | re.M))
pattern = re.compile(r"^.*error.*$", re.I | re.M)
print(len(pattern.findall(log)), "error lines")
[] ['ERROR', 'Error'] ['2026-03-02 09:14:07', '2026-03-02 09:14:09'] 2 error lines
The first search finds nothing, because the log only contains
ERROR and Error. Compiling the pattern
once with re.compile() stores the flags in the pattern
object, so you do not have to repeat them at every call.
In JavaScript the flags are letters after the closing slash of a
regex literal, or a second string argument to the
RegExp constructor. The flags property
shows them, always in alphabetical order.
const line = '2026-03-02 09:14:07 portal ERROR login failed';
console.log(/error/.test(line));
console.log(/error/i.test(line));
const level = new RegExp('error|warn', 'gi');
console.log(level.flags, level.ignoreCase, level.global);
console.log(line.match(level));
false true gi true true [ 'ERROR' ]
PHP's preg_* functions use the Perl style: the pattern
sits between two delimiters, usually slashes, and the modifiers
follow the closing one. On the command line, flags are ordinary
options, such as grep -i, or a letter after a
sed command.
<?php
$line = '2026-03-02 09:14:07 portal ERROR login failed';
var_dump(preg_match('/error/', $line));
var_dump(preg_match('/error/i', $line));
var_dump(preg_match('/^\d{4}-\d{2}-\d{2} .* error /i', $line));
int(0) int(1) int(1)
printf '%s\n' 'portal ERROR login failed' 'portal Error timeout' \
'meter warning delayed' > portal.log
grep -c 'error' portal.log
grep -c -i 'error' portal.log
sed 's/error/[E]/I' portal.log
0 2 portal [E] login failed portal [E] timeout meter warning delayed
The I after the sed command is a GNU sed extension;
other sed versions may not support it.
Flags at a glance
| Meaning | Python | JavaScript | PHP (PCRE) | Shell |
|---|---|---|---|---|
| Ignore case | re.I |
i |
i |
grep -i, sed I |
^ and $ per line |
re.M |
m |
m |
always (line by line) |
| Dot matches newline | re.S |
s |
s |
not applicable |
| Spaces and comments in pattern | re.X |
none | x |
none |
| Unicode handling | default for text; re.A for ASCII only |
u or v |
u |
follows the locale |
| All matches | findall(), finditer() |
g |
preg_match_all() |
sed g, grep -o |
| Match only at a given position | pattern.match(text, pos) |
y |
\G in the pattern |
none |
Case-insensitive matching
With
case-insensitive matching
each letter in the pattern also matches its other-case forms, so
error matches error,
Error and ERROR. That also applies inside
character classes: with re.I, [A-Z]+ also matches
portal. Log levels, host names, email domains and HTTP
header names are typical places where you want it.
Case is not only an ASCII matter. What counts as "the same letter"
depends on how much Unicode the engine knows, and engines differ. A
neat test is the Kelvin sign K (U+212A), which Unicode treats as
another uppercase form of k:
import re
text = "k K \u212a" # small k, capital K, Kelvin sign
print(len(re.findall(r"k", text, re.I)))
print(len(re.findall(r"k", text, re.I | re.ASCII)))
3 2
const text = 'k K \u212a'; // small k, capital K, Kelvin sign
console.log(text.match(/k/gi).length);
console.log(text.match(/k/giu).length);
2 3
Python applies the Unicode case rules to text patterns unless you
add re.ASCII. JavaScript without a Unicode flag uses
simpler rules that miss the Kelvin sign; with u (or
v) it uses Unicode case folding. PHP behaves like
JavaScript: /k/i finds two, /k/iu three.
For log searching this hardly matters, but for user names and other
identifiers it can: two names that look different may match
case-insensitively.
Multiline mode
By default ^ and $ are
anchors for
the start and end of the whole text. Multiline mode (Python
re.M, JavaScript and PHP m) makes them
match at the start and end of every line instead. The name confuses
many people: the flag does not let a match span several lines, it
only changes the two anchors. The details, including
\A, \z and the final-newline trap, are on
Anchors and Word Boundaries.
const syslog = `Mar 2 09:14:07 noc-fw1 link down on ge-0/0/1
Mar 2 09:14:09 noc-fw1 link up on ge-0/0/1
Mar 2 09:20:41 noc-sw3 link down on xe-1/0/4`;
console.log(syslog.match(/^Mar 2 \S+ (\S+) link down/g));
console.log(syslog.match(/^Mar 2 \S+ (\S+) link down/gm));
[ 'Mar 2 09:14:07 noc-fw1 link down' ] [ 'Mar 2 09:14:07 noc-fw1 link down', 'Mar 2 09:20:41 noc-sw3 link down' ]
Without m, ^ can only match at position 0,
so only the first line can ever qualify. Tools such as
grep and sed already work line by line, so
on the command line ^ and $ always mean
the start and end of the line.
Dotall mode
The dot matches any character except a line break. That
default is deliberate: .* then stops at the end of the
line, which is what you want when you scan a log line by line.
Dotall mode (Python
re.S or re.DOTALL, JavaScript and PHP
s) lets the dot match newlines too, so a match can run
across lines. You need it for multi-line records such as an alert
with a body, an email header block or a stack trace.
import re
alert = """ALERT meter MTR-0042
reason: no reading received
since: 2026-03-02T09:00:00Z
END"""
print(re.search(r"ALERT.*END", alert))
print(re.search(r"ALERT.*END", alert, re.S) is not None)
print(re.search(r"reason: (.*)", alert).group(1))
print(re.search(r"reason: (.*)", alert, re.S).group(1).count("\n"))
None True no reading received 2
The last line shows the price: with re.S,
(.*) no longer stops at the end of the line but eats
everything up to the end of the text (two more lines). Turn dotall
on only for the part that should cross lines, or use a lazy
quantifier (see
Quantifiers).
Before ES2018 JavaScript had no s flag, so older code
uses [\s\S] ("any whitespace or any non-whitespace",
which is every character) as a dot that crosses lines. Both still
work:
const alert = 'ALERT meter MTR-0042\nreason: no reading\nEND';
console.log(/ALERT.*END/.test(alert));
console.log(/ALERT.*END/s.test(alert));
console.log(/ALERT[\s\S]*END/.test(alert));
false true true
Verbose mode
Regular expressions are compact, which makes long ones hard to read.
In verbose mode (Python
re.X or re.VERBOSE, PHP and PCRE
x) the engine ignores whitespace in the pattern and
treats # up to the end of the line as a comment. You
can then spread a pattern over several lines and explain each part.
import re
LOG_LINE = re.compile(r"""
^(?P<date>\d{4}-\d{2}-\d{2}) # date, ISO 8601
\s+(?P<time>\d{2}:\d{2}:\d{2}) # time, 24-hour clock
\s+(?P<service>[a-z]+) # portal, meter or noc
\s+(?P<level>[A-Z]+) # INFO, WARN or ERROR
\s+(?P<message>.*)$ # everything else
""", re.VERBOSE)
m = LOG_LINE.match("2026-03-02 09:14:07 portal ERROR login failed for INC-31877")
print(m["service"], m["level"])
print(m["message"])
portal ERROR login failed for INC-31877
Because spaces are ignored, a space you do want to match
must be written differently: as \s, [ ] or
an escaped space \ . Forgetting that is the classic
verbose-mode bug: the pattern below silently becomes
KestrelEnergy.
import re
print(re.search(r"Kestrel Energy", "Kestrel Energy", re.X))
print(re.search(r"Kestrel[ ]Energy", "Kestrel Energy", re.X) is not None)
None True
PHP's x modifier works the same way. Here it documents
a pattern for Dutch postal codes (four digits, the first not zero,
an optional space, two capital letters):
<?php
$postalCode = '/^
[1-9][0-9]{3} # four digits, no leading zero
[ ]? # optional space
[A-Z]{2} # two letters
$/x';
foreach (['2511 BT', '2511BT', '0511 BT', '2511 bt'] as $input) {
echo $input, ' => ', preg_match($postalCode, $input), "\n";
}
2511 BT => 1 2511BT => 1 0511 BT => 0 2511 bt => 0
Unicode mode
Text is more than ASCII, and engines differ in how much of
Unicode
they apply by default. The Unicode flag decides what the dot, the
shorthands such as \d and \w, and
case-insensitive matching mean.
-
Python: patterns on
strare Unicode by default;\dmatches any decimal digit, including Arabic-Indic٤٢.re.ASCII(re.A) limits\d,\w,\sand\bto ASCII. -
JavaScript:
\dand\ware always ASCII, even withu(withiandutogether,\walso matches ſ and the Kelvin sign). Theuflag makes the dot match a whole code point instead of half of a surrogate pair, enables\p{…}property escapes and turns sloppy syntax into errors. The newervflag (ES2024) does all that plus set operations in classes. -
PHP: without
uthe pattern and the subject are treated as bytes, soé(two bytes in UTF-8) is two "characters". Withuboth are UTF-8, and on this machine (PHP 8.2, PCRE2 10.42)\dthen matches Unicode digits as well. An invalid UTF-8 subject makes the function fail.
import re
reading = "Meter 42, display shows ٤٢"
print(re.findall(r"\d+", reading))
print(re.findall(r"\d+", reading, re.ASCII))
['42', '٤٢'] ['42']
const plug = '🔌'; // one emoji, two UTF-16 code units
console.log(plug.length);
console.log(plug.match(/./)[0].length);
console.log(plug.match(/./u)[0].length);
console.log('Meter 42, display shows ٤٢'.match(/\d+/gu));
2 1 2 [ '42' ]
<?php
$reading = 'Meter 42, display shows ٤٢';
echo preg_match_all('/\d/', $reading), "\n";
echo preg_match_all('/\d/u', $reading), "\n";
var_dump(preg_match('/./u', "caf\xE9")); // Latin-1 byte, not UTF-8
echo preg_last_error_msg(), "\n";
2 4 bool(false) Malformed UTF-8 characters, possibly incorrectly encoded
Note the last case: preg_match() returns
false, not 0. That means "the function
failed", not "no match". Always compare with === and
check preg_last_error_msg() when the input may not be
valid UTF-8. Character classes and \p{…} are covered on
Character Classes and Shorthands.
Global and sticky matching
In JavaScript the global flag
g means "all matches, not just the first". It changes
what several methods do: match() returns an array of
all matched strings instead of one match with its groups,
replace() replaces every match instead of the first,
and matchAll() and replaceAll() refuse a
regex without g.
To make that work, a regex with g (or y)
keeps state: its lastIndex property says where the next
search starts. exec() and test() update it
after each call. Reuse the same regex object on another string and
it starts in the middle:
const ticket = /INC-\d+/g;
console.log(ticket.test('INC-31877'), ticket.lastIndex);
console.log(ticket.test('INC-31902'), ticket.lastIndex);
console.log(ticket.test('INC-31902'), ticket.lastIndex);
console.log('INC-31877 and INC-31902'.match(/INC-\d+/));
console.log('INC-31877 and INC-31902'.match(/INC-\d+/g));
true 9 false 0 true 9 [ 'INC-31877', index: 0, input: 'INC-31877 and INC-31902', groups: undefined ] [ 'INC-31877', 'INC-31902' ]
The second test() starts at position 9, which is
already the end of the new string, so it fails and resets
lastIndex to 0; the third succeeds again. The fix: do
not use g for a yes/no check, or create the regex
inside the function so every call starts fresh.
The sticky flag
The sticky flag
y also uses lastIndex, but more strictly:
the match must start exactly at that position. A global
search skips ahead to the next place the pattern fits; a sticky
search does not skip anything. That makes y the tool
for tokenizers and parsers, which must consume the input piece by
piece and notice when something unexpected sits in between.
g the engine searches forward from
lastIndex and silently skips text that does not fit;
with y it tries only at lastIndex and
stops at the first gap.
const text = 'INC-31877 CHG-0042 oops REQ-0007';
for (const flags of ['g', 'y']) {
const token = new RegExp('(?:INC|CHG|REQ)-\\d+ ?', flags);
const found = [];
let m;
while ((m = token.exec(text)) !== null) {
found.push(m[0].trim());
}
console.log(flags, found);
}
g [ 'INC-31877', 'CHG-0042', 'REQ-0007' ] y [ 'INC-31877', 'CHG-0042' ]
Python has the same idea without a flag: a compiled pattern's
match(text, pos) only tries at position
pos. In PCRE the assertion \G ("where the
previous match ended") plays that role.
Inline flags
Sometimes you cannot pass a flag: the pattern lives in a configuration file, a database column or a web form that only takes the pattern text. An inline flag puts the flag inside the pattern itself:
-
(?i)at the start turns case-insensitive matching on for the whole pattern. Letters can be combined:(?im). -
(?i:…)turns it on only for the group;(?-i:…)turns it off for the group.
import re
log = "ERROR DB, error x, Error NOC, warn y"
print(re.findall(r"(?i)error \w+", log))
print(re.findall(r"(?i:error) [A-Z]+", log))
print(re.findall(r"(?i)error(?-i: [A-Z]+)", log))
['ERROR DB', 'error x', 'Error NOC'] ['ERROR DB', 'Error NOC'] ['ERROR DB', 'Error NOC']
In the last two patterns only the word error ignores
case; the name after it must be in capitals, so
error x is not found. Python 3.11 requires a
global inline flag such as (?i) at the very
start of the pattern; anywhere else it is an error:
import re
re.compile(r"portal (?i)error")
re.error: global flags not at the start of the expression at position 7
PCRE is more relaxed: (?i) in the middle of a pattern
applies from that point to the end of the enclosing group. That is
the flavor behind PHP and grep -P:
printf '%s\n' 'portal ERROR login failed' 'PORTAL error timeout' \
'portal warning' > portal.log
grep -P '(?i)error' portal.log
echo ---
grep -P 'portal (?i)error' portal.log
portal ERROR login failed PORTAL error timeout --- portal ERROR login failed
JavaScript is the exception. Node.js 20 rejects every inline flag:
const level = /(?i)error/;
console.log(level.test('ERROR'));
SyntaxError: Invalid regular expression: /(?i)error/: Invalid group
ES2025 added modifier groups such as
(?i:error) to JavaScript. They only exist in the group
form, and Node.js 20 does not support them yet, so check your
runtime before relying on them. A global (?i) at the
start is not part of JavaScript: flags for the whole pattern always
go after the closing slash.
Other flags worth knowing
| Flag | Where | Effect |
|---|---|---|
d |
JavaScript (ES2022) |
Adds indices to a match: start and end
positions of the whole match and of every group.
|
U |
PHP |
Inverts greediness: .+ becomes lazy and
.+? greedy. Confusing for readers; prefer
writing ? where you mean lazy.
|
D |
PHP |
$ matches only at the very end, not before a
final newline (ignored when m is set).
|
n |
PHP 8.2+ | Plain parentheses no longer capture; only named groups do. |
re.DEBUG |
Python | Prints how the pattern was parsed; handy for debugging. |