Regex: the parts worth remembering

Notes on regular expressions, after watching the data wrangling lecture from MIT's Missing Semester course. The short version: five concepts cover almost everything.

1. Character sets — what to match

Most characters in a regex simply match themselves — the pattern cat matches the string "cat". The special syntax is for when you don't know the exact character — "any digit", "a or e", "anything at all":

  • . — any single character
  • [ae] — one of these characters (gr[ae]y matches gray and grey)
  • [a-z0-9] — one from these ranges
  • [^abc] — one character that is not any of these
  • \d digit, \w word character (letters, digits, _), \s whitespace

2. Quantifiers — how many

A quantifier applies to whatever is right before it:

  • ? — zero or one (colou?r matches color and colour)
  • * — zero or more
  • + — one or more
  • {4} / {2,4} — exactly / between (\d{4} matches a year)

One surprise to know about: quantifiers are greedy — they match as much as they can, not as little. Search <b>bold</b> with the pattern <.*>, expecting to grab the <b> tag. Instead you get the whole string: .* doesn't stop at the first > it meets — it swallows everything it can, as long as one > is left over for the end of the pattern. So the pattern's > ends up matching the last > in the string. Putting ? after a quantifier flips it to lazy — match as little as possible:

<.*>   on <b>bold</b>   →   <b>bold</b>   (greedy: as much as possible)
<.*?>  on <b>bold</b>   →   <b>           (lazy: as little as possible)

3. Anchors — where to match

By default a pattern matches anywhere in the text. Anchors pin it down:

  • ^ — start of line (^# finds Markdown headings)
  • $ — end of line (\.md$ finds Markdown files)

4. Groups and alternation — either, or

| means "or", and parentheses limit its reach: \.(jpg|png|gif)$ matches image file extensions. Without the parentheses, \.jpg|png would happily match plain png anywhere.

5. Capture groups — reuse what matched

Parentheses don't just group — they capture. Whatever matched inside ( ) is available in a replacement as \1, \2, ... in order. This turns find-and-replace into text surgery:

s/(\d{4})-(\d{2})-(\d{2})/\3\/\2\/\1/

turns 2026-08-03 into 03/08/2026 — same digits, rearranged by group number. The same idea works in Vim's :%s, in sed, and in any IDE's regex find-and-replace.

Now read this

One line to finish — a regex that checks whether a string is a hex color code like #fff or #ffa142:

^#([0-9a-f]{3}|[0-9a-f]{6})$

Anchored at both ends (^ $), a literal #, then one group with two alternatives: exactly three hex digits ([0-9a-f]{3}) or exactly six. Character sets, quantifiers, anchors, groups, alternation — everything above, one line.