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.
The flag table
Section titled “The flag table”| 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
gflag. “Global” is a property of the function you call, not the pattern.re.searchfinds one,re.findall/re.finditer/re.subfind all. - JavaScript has no
xflag. Long patterns cannot be commented inline; build them from string pieces instead.
Setting flags
Section titled “Setting flags”/abc/gi // literal syntaxnew RegExp('abc', 'gi') // constructorconst re = /abc/gi;re.flags // => "gi"re.global // => truere.ignoreCase // => trueimport rere.search(r"abc", s, re.I) # per-callre.search(r"abc", s, re.I | re.M) # combine with |p = re.compile(r"abc", re.IGNORECASE) # precompiledp.flags # => 34 (includes re.UNICODE)Inline flags
Section titled “Inline flags”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")) # => Truebool(re.search(r"a(?i:bc)", "aBC")) # => TrueJavaScript 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.
# grep -P takes inline flags because the pattern is a stringprintf '2024-01\n' | grep -oP '(?x) \d{4} - \d{2}'# => 2024-01i — case insensitivity
Section titled “i — case insensitivity”Straightforward until Unicode gets involved.
'A'.match(/a/i) // => ["A"]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"}'# => matchCase 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') // => truere.lastIndex // => 2re.test('a1') // => false! starts searching from index 2re.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, finePython has no equivalent state — the function chooses the scope:
import rere.search(r"\d+", "a1 b22").group() # => '1' first match onlyre.findall(r"\d+", "a1 b22") # => ['1', '22']re.sub(r"\d+", "N", "a1 b22") # => 'aN bN' all by defaultre.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”:
echo 'aaa' | sed -E 's/a/b/' # => baa (first only, the default)echo 'aaa' | sed -E 's/a/b/g' # => bbbecho 'aaa' | sed -E 's/a/b/2' # => aba (second occurrence only)echo 'aaa' | sed -E 's/a/b/2g' # => abb (second onwards)m — multiline anchors
Section titled “m — multiline anchors”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:
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 stringprintf 'a\nb\n' | sed -zE 's/a\nb/X/' # GNU sed: -z uses NUL as the separatorre.fullmatch(r"\d+", "123") # => matchre.fullmatch(r"\d+", "123a") # => Nones — dotAll
Section titled “s — dotAll”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') // => truebool(re.search(r"a.b", "a\nb")) # => Falsebool(re.search(r"a.b", "a\nb", re.S)) # => TrueA portable alternative when the flag is unavailable: [\s\S] (or [^] in JavaScript only) matches literally any character including newlines.
x — extended / verbose
Section titled “x — extended / verbose”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 reLOG = 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(''));Unicode
Section titled “Unicode”JavaScript: the u and v flags
Section titled “JavaScript: the u and v flags”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 pointu 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') // => trueCommon 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') // => falseUse v for new code where support allows; it is strictly better than u.
Python: Unicode by default
Section titled “Python: Unicode by default”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.
printf 'héllo\n' | grep -oP '\p{L}+' # => hélloPOSIX 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:
LC_ALL=C grep -E '^[[:alnum:]]+$' huge.txtBRE vs ERE: exactly what must be escaped
Section titled “BRE vs ERE: exactly what must be escaped”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.
printf 'aaa\n' | grep -c 'a\{2,\}' # => 1 BREprintf 'aaa\n' | grep -c 'a{2,}' # => 0 BRE: literal braces, no matchprintf '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 literalecho 'aaa' | sed -E 's/a+/X/' # => X ERE
grep 'cat\|dog' file.txt # BREgrep -E 'cat|dog' file.txt # EREWhat 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:
printf '42 ab\n' | grep -oE '\w+' # => 42 / ab \w worksprintf 'a b\n' | grep -oE 'a\sb' # => a b \s worksprintf 'cat\n' | grep -oE '\bcat\b' # => cat \b worksprintf '42\n' | grep -oE '\d+'# => grep: warning: stray \ before d \d does NOT workecho 'a1' | sed -E 's/\d//' # => a1 no changeecho 'a1' | sed -E 's/[0-9]//' # => aecho 'a1' | gawk '{gsub(/\d/,""); print}'# => gawk: warning: regexp escape sequence `\d' is not a known regexp operatorAlways 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 specifics
Section titled “awk specifics”awk uses ERE, with three differences worth memorising:
\bis backspace, not a word boundary. Use gawk’s\y, or\<and\>.- No capture groups in
sub/gsubreplacements.&is the whole match;\1does nothing. Usematch()+substr(), or gawk’s three-argumentmatch(str, re, arr). - Dynamic regexes are strings, so backslashes are consumed twice:
$0 ~ "\\."needs the doubled backslash, while$0 ~ /\./does not.
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 1Quick engine capability matrix
Section titled “Quick engine capability matrix”| 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 |
Key points
Section titled “Key points”gis 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. maffects^and$only;saffects.only.- Python’s
$matches before a trailing newline; JavaScript’s does not. Use\Zorre.fullmatchfor strict validation. x/re.VERBOSEmakes 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;\dand\wstay ASCII regardless. - Python is Unicode-aware by default but has no
\p{...}— use theregexmodule. - In BRE,
+ ? { } ( ) |are literal and need backslashes; in ERE they are metacharacters. Prefer-E. \ddoes not work in GNUgrep -E,sed -E, orawk. Use[0-9].