本页面有中文版。
Cette page est disponible en français.
Esta página está disponible en español.
Guides1 live hunt inside

Regex flags explained: i, g, m, s, u, y and d

What each JavaScript regex flag changes, with live examples: case-insensitive i, global g (and the lastIndex trap), multiline m, dotAll s, unicode u, sticky y and indices d, plus how Python spells the same ideas.

Regex Hunter logo

· 4 min read

/^colou?r$/i

match

  • color
  • Colour
  • COLOR
  • colour

spare

  • colr
  • colours
  • discolor
  • kolor

A flag changes how the whole pattern behaves. In JavaScript, flags go after the closing slash: /cat/gi. In Python they're arguments, like re.IGNORECASE, or inline at the start of the pattern, like (?i).

There are seven in JavaScript. Two you'll use constantly, a few you'll need now and then, and one has a trap that has caught nearly everyone. Every example is live, so edit the patterns and watch the lines change.

01i: ignore case

Letters match in either case:

Matches5/5 as expected
/^colou?r$/i

Should match3/3

  • color
  • Colour
  • COLOR

Should not2/2

  • colours
  • discolor
Try it in the Playground
With i, the pattern matches any mix of upper and lower case.

It saves writing [Cc][Oo][Ll]…, and it also applies inside classes: with i, [a-z] accepts capitals too.

Python: re.IGNORECASE, re.I, or (?i).

02g: global

Without g, a regex finds the first match and stops. With it, methods like match, matchAll and replace work on every match:

"a1b22c333".match(/\d+/);    // ["1"], plus index and input
"a1b22c333".match(/\d+/g);   // ["1", "22", "333"]
"a-b-c".replace(/-/, "+");   // "a+b-c"
"a-b-c".replace(/-/g, "+");  // "a+b+c"

Python has no g. Instead you pick the function: re.search finds one match, re.findall and re.finditer find all of them, and re.sub replaces all by default.

The lastIndex trap

A regex with g (or y) remembers where its last match ended, in lastIndex, and the next test() starts from there. Reuse one such regex on several strings and results start to alternate:

const hasDigit = /\d/g;

hasDigit.test("a1");  // true, lastIndex is now 2
hasDigit.test("b2");  // false: the search starts at index 2, past the "2"
hasDigit.test("b2");  // true again, lastIndex had reset to 0

The fix is simple: don't put g on a regex you only use with test(). You don't need it to check whether something matches.

03m: multiline

^ and $ match at the start and end of each line instead of the whole text. The post on anchors and multiline mode covers it in full.

"one\ntwo".match(/^\w+$/gm);  // ["one", "two"]

Python: re.MULTILINE, re.M, or (?m).

04s: dotAll

Normally . matches any character except a line break. With s, it matches line breaks too:

/start.*end/.test("start\nend");   // false
/start.*end/s.test("start\nend");  // true

Before s existed, people wrote [\s\S] for "any character at all", and you'll still see it in older code.

Python: re.DOTALL, re.S, or (?s).

05u: unicode

u makes the pattern work in whole Unicode characters instead of UTF-16 pieces, and it turns on property escapes like \p{L} (any letter, in any script):

Matches6/6 as expected
/^\p{L}+$/u

Should match4/4

  • hello
  • café
  • Zürich
  • Москва

Should not2/2

  • R2D2
  • two words
Try it in the Playground
\p{L} is a letter in any alphabet, accents included. [a-z] would reject all but the first line.

It also makes . match one emoji instead of half of one, and it makes the parser strict: an unnecessary escape like \- outside a class becomes a syntax error instead of being quietly ignored. JavaScript's newer v flag is a stricter superset of u with set operations inside classes.

Python's re is Unicode-aware by default but has no \p{…}. The third-party regex module adds it.

06y: sticky

With y, a match must start exactly at lastIndex, not anywhere after it. It's mostly for tokenizers that walk through a string piece by piece:

const word = /\w+/y;

word.lastIndex = 0;
word.exec("hi there");  // ["hi"], lastIndex is now 2
word.exec("hi there");  // null: index 2 is a space, and y won't skip ahead

07d: indices

d adds an indices array to each match with the start and end of the match and of every group:

const m = /(\d+)-(\d+)/d.exec("pages 12-30");

m.indices[0];  // [6, 11], the whole match
m.indices[1];  // [6, 8], the first group, "12"

Handy for highlighting matches in an editor. Python has this built in: m.span(1).

08Try one

Use the i flag to keep this one short:

Live huntpar 5

Catch yes in any case, and nothing else.

Targets · 0/5 caught

  • yestarget
  • Yestarget
  • YEStarget
  • yEstarget
  • yeStarget

Innocents · 0 hit

  • yesterdayinnocent
  • eyesinnocent
  • noinnocent
  • yeinnocent
  • y e sinnocent

Type a regex — the board updates as you go.

Try it in the Playground
Like this? A new one drops every day.Play today’s hunt

09Checklist

  • i ignores case. m makes ^ and $ per line. s lets . match line breaks.
  • g finds every match, but don't use it with test() on a reused regex.
  • u handles full Unicode and enables \p{L}.
  • y matches only at lastIndex. d adds match positions.
  • Python: re.I, re.M, re.S, or inline (?i), (?m), (?s). No g: use findall or sub.

10Keep practising