Skip to content

Flags & Engine Differences

Flags (also called modifiers) change how the whole pattern is interpreted. Every engine has them, none of them agree on the full set, and a couple of the “same” flags behave differently.

Flag Name JavaScript Python re PCRE2 inline grep / sed
i case-insensitive /x/i re.I / re.IGNORECASE (?i) grep -i, sed 's///I'
g global (all matches) /x/g not a flag — use findall/sub not a flag sed 's///g'
m multiline anchors /x/m re.M / re.MULTILINE (?m) n/a (line-oriented already)
s dot matches newline /x/s re.S / re.DOTALL (?s) n/a
x extended / verbose not supported re.X / re.VERBOSE (?x) grep -P '(?x)…'
u Unicode semantics /x/u default for str; re.A opts out (?u) / PCRE2_UTF locale-driven
y sticky /x/y
d match indices /x/d — (use .span())
v Unicode sets /x/v (ES2024)

Two structural facts follow from that table:

  • Python has no g flag. “Global” is a property of the function you call, not the pattern. re.search finds one, re.findall/re.finditer/re.sub find all.
  • JavaScript has no x flag. Long patterns cannot be commented inline; build them from string pieces instead.
JavaScript
/abc/gi // literal syntax
new RegExp('abc', 'gi') // constructor
const re = /abc/gi;
re.flags // => "gi"
re.global // => true
re.ignoreCase // => true
Python
import re
re.search(r"abc", s, re.I) # per-call
re.search(r"abc", s, re.I | re.M) # combine with |
p = re.compile(r"abc", re.IGNORECASE) # precompiled
p.flags # => 34 (includes re.UNICODE)

Flags can also be embedded in the pattern itself. This is the only way to set them when the pattern travels as a plain string (config files, grep -P, database columns).

bool(re.search(r"(?i)abc", "ABC")) # => True
# Scoped inline flags — apply to one group only (Python 3.6+)
bool(re.search(r"a(?i:BC)", "aBC")) # => True
bool(re.search(r"a(?i:bc)", "aBC")) # => True

JavaScript supports scoped inline modifiers (?i:...) and (?-i:...) in recent engines (ES2025 “modifiers”), but not the global (?i) form. If you target older runtimes, pass flags to RegExp instead.

Terminal window
# grep -P takes inline flags because the pattern is a string
printf '2024-01\n' | grep -oP '(?x) \d{4} - \d{2}'
# => 2024-01

Straightforward until Unicode gets involved.

'A'.match(/a/i) // => ["A"]
Terminal window
printf 'Cat\ncat\nconcat\n' | grep -i 'CAT'
# => Cat
# => cat
# => concat
echo 'Cat' | sed -E 's/cat/dog/I' # GNU sed: I flag on the s/// command
# => dog
echo 'Cat' | gawk 'BEGIN{IGNORECASE=1} /cat/{print "match"}'
# => match

Case folding is locale- and Unicode-dependent for non-ASCII text. Turkish dotless ı, German ß vs SS, and Greek final sigma all behave in ways plain i does not fully capture. For user-facing comparisons, normalize with toLocaleLowerCase() / str.casefold() rather than relying on regex i.

g — global, and the JavaScript lastIndex trap

Section titled “g — global, and the JavaScript lastIndex trap”

In JavaScript, g does two things: it makes replace/match operate on all matches, and it makes the regex object stateful.

'aAa'.replace(/a/gi, '-') // => "---"
'a1 b2'.match(/\w\d/g) // => ["a1", "b2"]

The stateful part is a genuine footgun. A g (or y) regex carries a mutable lastIndex, and test/exec advance it:

const re = /\d+/g;
re.test('a1') // => true
re.lastIndex // => 2
re.test('a1') // => false! starts searching from index 2
re.lastIndex // => 0 (reset after the failure)

String.prototype.replaceAll and matchAll go the other way and require g:

'abc'.matchAll(/b/) // => TypeError: matchAll must be called with a global RegExp
'abc'.matchAll(/b/g) // => iterator, fine

Python has no equivalent state — the function chooses the scope:

import re
re.search(r"\d+", "a1 b22").group() # => '1' first match only
re.findall(r"\d+", "a1 b22") # => ['1', '22']
re.sub(r"\d+", "N", "a1 b22") # => 'aN bN' all by default
re.sub(r"\d+", "N", "a1 b22", count=1) # => 'aN b22'

In sed, g is a flag on the s command, and a bare number means “the Nth occurrence”:

Terminal window
echo 'aaa' | sed -E 's/a/b/' # => baa (first only, the default)
echo 'aaa' | sed -E 's/a/b/g' # => bbb
echo 'aaa' | sed -E 's/a/b/2' # => aba (second occurrence only)
echo 'aaa' | sed -E 's/a/b/2g' # => abb (second onwards)

m changes what ^ and $ mean: line boundaries instead of string boundaries. It does not affect ..

'a\nb'.match(/^b$/) // => null
'a\nb'.match(/^b$/m) // => ["b"]
re.findall(r"^b$", "a\nb") # => []
re.findall(r"^b$", "a\nb", re.M) # => ['b']

grep, sed, and awk process one line at a time, so ^ and $ are already line anchors and there is nothing to toggle. Getting a multi-line match out of them requires explicitly joining lines first:

Terminal window
printf 'a\nb\n' | sed -E 'N;s/a\nb/X/'
# => X
# N appends the next line into the pattern space, so \n becomes matchable
# Or read the whole file as one string
printf 'a\nb\n' | sed -zE 's/a\nb/X/' # GNU sed: -z uses NUL as the separator
re.fullmatch(r"\d+", "123") # => match
re.fullmatch(r"\d+", "123a") # => None

Makes . match newlines too. Named dotAll in JavaScript (ES2018), re.DOTALL in Python.

/a.b/.test('a\nb') // => false
/a.b/s.test('a\nb') // => true
bool(re.search(r"a.b", "a\nb")) # => False
bool(re.search(r"a.b", "a\nb", re.S)) # => True

A portable alternative when the flag is unavailable: [\s\S] (or [^] in JavaScript only) matches literally any character including newlines.

Whitespace in the pattern is ignored and # starts a comment. This is how a long regex becomes readable. Python and PCRE have it; JavaScript does not.

import re
LOG = re.compile(r"""
^(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # client IP
\s+-\s+-\s+
\[(?P<ts>[^\]]+)\] # timestamp in brackets
\s+"(?P<method>[A-Z]+)\s
(?P<path>\S+) # request path
""", re.X)
m = LOG.match('10.0.0.1 - - [10/Oct/2024:13:55:36] "GET /index.html HTTP/1.1"')
m.group('method'), m.group('path') # => ('GET', '/index.html')

JavaScript’s workaround is to build the pattern from an array of strings:

const LOG = new RegExp([
String.raw`^(?<ip>\d{1,3}(?:\.\d{1,3}){3})`, // client IP
String.raw`\s+-\s+-\s+`,
String.raw`\[(?<ts>[^\]]+)\]`, // timestamp
].join(''));

Without u, a JavaScript regex operates on UTF-16 code units, so an emoji or any character above U+FFFF counts as two.

'😀'.length // => 2
/^.$/.test('😀') // => false — one dot, two code units
/^.$/u.test('😀') // => true — u makes . a code point

u also enables \p{...} property escapes and makes unknown escapes an error (which catches typos).

/\p{Lu}/u.test('Ä') // => true uppercase letter
/\p{Script=Greek}/u.test('λ') // => true
/^\p{L}+$/u.test('héllo') // => true

Common property names: \p{L} letter, \p{Lu}/\p{Ll} case, \p{N}/\p{Nd} number/decimal digit, \p{P} punctuation, \p{Zs} space separator, \p{Script=Han}, \p{Emoji_Presentation}. \P{...} is the negation.

The v flag (ES2024) is a superset of u: same Unicode semantics, plus set operations inside character classes and correct handling of multi-code-point graphemes.

/^[\p{L}--[\p{Lu}]]+$/v.test('abc') // => true letters minus uppercase
/^[\p{L}--[\p{Lu}]]+$/v.test('Abc') // => false

Use v for new code where support allows; it is strictly better than u.

For str patterns, Python 3 is Unicode-aware out of the box — re.UNICODE is implied and re.ASCII opts back out.

re.findall(r"\d", "٣ 3") # => ['٣', '3']
re.findall(r"\d", "٣ 3", re.ASCII) # => ['3']

\w likewise matches accented letters, CJK, and so on. For bytes patterns, everything is ASCII-only and re.ASCII is implied.

grep -P gets PCRE2’s Unicode properties; grep -E does not.

Terminal window
printf 'héllo\n' | grep -oP '\p{L}+' # => héllo

POSIX classes are locale-aware in principle — [[:alpha:]] depends on LC_CTYPE. Setting LC_ALL=C makes matching ASCII-only and noticeably faster, which is a common trick for large grep jobs:

Terminal window
LC_ALL=C grep -E '^[[:alnum:]]+$' huge.txt

This is the difference that trips up everyone moving between grep and grep -E.

Construct ERE (grep -E, sed -E, awk) BRE (grep, sed default)
one or more + \+ (GNU ext; not POSIX)
optional ? \? (GNU ext; not POSIX)
repetition {2,5} \{2,5\}
grouping (...) \(...\)
alternation bare pipe backslash + pipe (GNU ext; not POSIX)
backreference \1 \1
anchors, ., *, [...] same same

The rule of thumb: in BRE the characters + ? { } ( ) | are literal, and a backslash turns them special. In ERE it is the other way round.

Terminal window
printf 'aaa\n' | grep -c 'a\{2,\}' # => 1 BRE
printf 'aaa\n' | grep -c 'a{2,}' # => 0 BRE: literal braces, no match
printf 'aaa\n' | grep -cE 'a{2,}' # => 1 ERE
echo 'aaa' | sed 's/a\+/X/' # => X BRE with GNU \+
echo 'a+b' | sed 's/a+/X/' # => Xb BRE: "+" is literal
echo 'aaa' | sed -E 's/a+/X/' # => X ERE
grep 'cat\|dog' file.txt # BRE
grep -E 'cat|dog' file.txt # ERE

What GNU adds to ERE — and what it does not

Section titled “What GNU adds to ERE — and what it does not”

GNU grep, sed, and gawk support some Perl shorthands as extensions, but the coverage is uneven and \d is not among them:

Terminal window
printf '42 ab\n' | grep -oE '\w+' # => 42 / ab \w works
printf 'a b\n' | grep -oE 'a\sb' # => a b \s works
printf 'cat\n' | grep -oE '\bcat\b' # => cat \b works
printf '42\n' | grep -oE '\d+'
# => grep: warning: stray \ before d \d does NOT work
Terminal window
echo 'a1' | sed -E 's/\d//' # => a1 no change
echo 'a1' | sed -E 's/[0-9]//' # => a
echo 'a1' | gawk '{gsub(/\d/,""); print}'
# => gawk: warning: regexp escape sequence `\d' is not a known regexp operator

Always write [0-9] or [[:digit:]] in shell regexes. And none of these extensions exist in BSD/macOS grep, so \w and \b are also unsafe for portable scripts — [[:alnum:]_] and \< \> are the safer choices.

awk uses ERE, with three differences worth memorising:

  1. \b is backspace, not a word boundary. Use gawk’s \y, or \< and \>.
  2. No capture groups in sub/gsub replacements. & is the whole match; \1 does nothing. Use match() + substr(), or gawk’s three-argument match(str, re, arr).
  3. Dynamic regexes are strings, so backslashes are consumed twice: $0 ~ "\\." needs the doubled backslash, while $0 ~ /\./ does not.
Terminal window
printf 'cat catalog\n' | gawk '{gsub(/\ycat\y/, "X"); print}'
# => X catalog
echo 'price 42' | awk '{gsub(/[0-9]+/, "[&]"); print}'
# => price [42]
echo 'a=1' | gawk 'match($0, /(\w+)=(\w+)/, a) { print a[1], a[2] }'
# => a 1
Feature JS Python re PCRE2 POSIX ERE RE2 / Go / Rust
\d \w \s yes (ASCII) yes (Unicode) yes no (GNU: partial) yes
Non-capturing (?:) yes yes yes no yes
Named groups (?<n>) (?P<n>) both no (?P<n>)
Backreferences yes yes yes yes no
Lookahead yes yes yes no no
Lookbehind yes, variable fixed width fixed width no no
Atomic (?>) / possessive no 3.11+ yes no n/a
\p{...} with u/v no yes no yes
Verbose x no yes yes no yes
Linear-time guarantee no no no depends yes
  • g is a JavaScript-only flag and it makes the regex object stateful — never reuse a /g/ regex with .test().
  • Python expresses “global” through the function (findall, finditer, sub), not a flag.
  • m affects ^ and $ only; s affects . only.
  • Python’s $ matches before a trailing newline; JavaScript’s does not. Use \Z or re.fullmatch for strict validation.
  • x / re.VERBOSE makes long patterns readable, but literal spaces must then be escaped. JavaScript has no equivalent.
  • JavaScript needs u (or better, v) for \p{...} and correct astral-plane handling; \d and \w stay ASCII regardless.
  • Python is Unicode-aware by default but has no \p{...} — use the regex module.
  • In BRE, + ? { } ( ) | are literal and need backslashes; in ERE they are metacharacters. Prefer -E.
  • \d does not work in GNU grep -E, sed -E, or awk. Use [0-9].