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 str are Unicode by default; \d matches any decimal digit, including Arabic-Indic ٤٢. re.ASCII (re.A) limits \d, \w, \s and \b to ASCII.
  • JavaScript: \d and \w are always ASCII, even with u (with i and u together, \w also matches ſ and the Kelvin sign). The u flag 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 newer v flag (ES2024) does all that plus set operations in classes.
  • PHP: without u the pattern and the subject are treated as bytes, so é (two bytes in UTF-8) is two "characters". With u both are UTF-8, and on this machine (PHP 8.2, PCRE2 10.42) \d then 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.

A global search skips the word oops and finds REQ-0007; a sticky search stops at oops INC-31877 CHG-0042 oops REQ-0007 g: match, match, skip, match skipped y: match, match, stop no match here
With 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.