Five things to look for when reading a regular expression

I put these notes together after watching MIT Missing Semester's Data Wrangling lecture. A regular expression can be hard to read when all its symbols are packed together. It helps to separate three questions: which characters can match, how many times they can repeat, and where they must appear.

Regular expression syntax varies between tools. These explanations use JavaScript regular expressions, while the date replacement example uses sed -E.

Which characters to match

cat matches the literal text cat. When a character can vary, use a character class. To match both gray and grey, put [ae] in the position that changes: gr[ae]y. [ae] matches one character, either a or e.

Pattern What it matches
. Any single character except a line terminator; the s flag includes line terminators
[ae] One a or e
[a-z0-9] One lowercase ASCII letter or digit
[^abc] One character other than a, b, or c
\d One digit; equivalent to [0-9]
\w Generally an ASCII letter, digit, or underscore; it does not include Hangul
\s A whitespace character, such as a space or line break

How many times to repeat

A character class describes what one character can be. A quantifier specifies how many times the preceding character or pattern can repeat.

  • ? means zero or one. colou?r matches both color and colour.
  • * means zero or more; + means one or more.
  • {4} means exactly four. \d{4} matches four digits.
  • {2,4} means between two and four repetitions.

You also need to consider how far a repeated pattern can match. If you use <.*> to find just <b> in <b>bold</b>, it matches the whole string. .* does not stop before the first >; it keeps matching up to the last one. Matching as much as possible this way is called greedy matching.

Greedy and lazy matching

<.*>   on <b>bold</b>   →   <b>bold</b>   (greedy)
<.*?>  on <b>bold</b>   →   <b>           (lazy)

Adding ? after the quantifier makes it match as little as possible. In this example, <.*?> stops at the first > and matches only <b>.

Where the match must appear

^ and $ mark the start and end of the input. ^# matches a # at the beginning, while \.md$ matches .md at the end. The backslash makes . a literal dot rather than a wildcard. To check the start and end of each line in multiline text, use the m flag. For example, you can use it with ^# to find Markdown headings.

Grouping alternatives

To match a string ending in jpg, png, or gif as its extension, use \.(jpg|png|gif)$. The parentheses group the alternatives, and | separates them. A dot must be followed by one of those three extensions, then the end of the string.

Removing the parentheses changes the scope. \.jpg|png matches either .jpg or png. The png does not need a dot before it, and more characters can follow. If a condition should apply to every alternative, check where the parentheses go.

Reusing what you matched

Parentheses can also store matched text in capture groups. By capturing the year, month, and day separately, you can rearrange them in a replacement. This example uses sed with -E to enable extended regular expressions.

Reorder a date with sed -E

printf '%s\n' '2026-08-03' | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'

The first group captures 2026, the second 08, and the third 03. Using \3, \2, and \1 in the replacement puts them in day/month/year order: 03/08/2026.

You can also reuse captured text in Vim's :%s or an IDE's find-and-replace tool. You need to adjust the syntax for each tool, though. sed uses \1 in replacements, JavaScript's replace() uses $1, and Vim requires backslashes before grouping parentheses in its default mode.

Reading a pattern for color codes

This pattern matches color codes such as #fff and #ffa142:

Three or six lowercase hexadecimal digits

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

^# checks the starting character, and [0-9a-f] allows digits and lowercase letters a through f. {3} and {6} specify three and six digits, with parentheses and | choosing between them. The final $ prevents a match if more characters follow. Uppercase letters are not included, so #FFF does not match.