regex · text · productivity
Regex: A Pocket Companion
The pattern language hiding inside grep, sed, your editor, and every language. Read it, write it, stop fearing it.
Regular expressions look like line noise until the handful of building blocks click — then
they’re everywhere: searching code, validating input, bulk-renaming, slicing logs. This is
the field guide to those building blocks, with patterns you can paste into grep, sed,
your editor, or any programming language.
# find every TODO comment in a codebase
$ grep -rn "TODO" .
# ...but with the power to match *patterns*, not just literal text.
The idea: literals plus metacharacters
A regex is mostly literal text — cat matches “cat” — sprinkled with metacharacters
that mean “a kind of thing” or “a repetition.” Learn maybe fifteen symbols and you can
read almost any pattern.
. any single character \ escape the next metacharacter
\d a digit (0-9) \w a word char (letter, digit, _)
\s whitespace \b a word boundary
Capitalised classes are the negation
\d is a digit; \D is not a digit. Likewise \w/\W and \s/\S. Flip the case
to flip the meaning.
Character classes: “one of these”
Square brackets match any single character from a set:
[aeiou] any vowel
[a-z] any lowercase letter
[A-Za-z0-9] a letter or digit
[^0-9] any character that is NOT a digit (^ inside [] means "not")
So [a-f0-9] matches one hex digit. Inside [...], most metacharacters lose their magic —
[.] is a literal dot.
Anchors: where, not just what
^ start of the line/string
$ end of the line/string
\b word boundary (edge of a word)
^Error matches “Error” only at the start of a line; \.txt$ matches a “.txt” only at the
end. \bcat\b matches the word “cat” but not “category” — boundaries are how you avoid
matching inside other words.
Quantifiers: how many
* zero or more + one or more ? zero or one (optional)
{3} exactly 3 {2,5} between 2 and 5 {2,} 2 or more
Combine a class with a quantifier and you’re describing real things:
\d{4} a four-digit year
\d{3}-\d{4} a 7-digit phone like 555-0142
colou?r matches "color" and "colour"
\s+ one or more spaces (collapse runs of whitespace)
Greedy by default — and it bites
Quantifiers are greedy: they grab as much as possible. In <a>x</a>, the pattern
<.*> matches the whole string, not just <a>. Add ? to make a quantifier
lazy (smallest match): <.*?> matches just <a>. When a pattern “matches too
much,” reach for *? or +?.
Groups and alternation
Parentheses group (and capture) part of a match; | means “or”:
(ab)+ one or more "ab"
(cat|dog|fish) any one of those words
(\d{4})-(\d{2}) capture a year and month separately
Captured groups are the heart of search-and-replace: you match with groups, then refer to
them in the replacement as \1, \2, … (or $1, $2 in some tools).
Putting it to work
Search a codebase with grep (use -E for the full syntax):
$ grep -rnE "TODO|FIXME|HACK" . # any of three markers
$ grep -E "^\s*def \w+\(" app.py # Python function definitions
$ grep -E "\b\d{1,3}(\.\d{1,3}){3}\b" access.log # IP addresses
Rewrite with sed — groups in the match, back-references in the replacement:
# swap "Last, First" into "First Last"
$ sed -E 's/(\w+), (\w+)/\2 \1/' names.txt
# strip trailing whitespace from every line
$ sed -E 's/\s+$//' file.txt
In your editor (Vim, VS Code), the same patterns power search-and-replace across a
file — capture with (...), reference with \1.
A few real patterns
# a loose email (good enough for a form, not for RFC lawyers)
[^@\s]+@[^@\s]+\.[^@\s]+
# an ISO date
\d{4}-\d{2}-\d{2}
# a hex color
#[0-9a-fA-F]{6}
# whole word, case-insensitive (with the /i flag or grep -i)
\berror\b
Don’t parse HTML or deeply-nested structures with regex
Regex matches patterns, not grammars. It’s perfect for tokens — emails, dates, log lines — and miserable at anything recursive like HTML or balanced brackets. For those, use a real parser. Knowing where regex stops being the right tool is half of using it well.
Pocket cheat-sheet
| Pattern | Matches |
|---|---|
. / \d / \w / \s |
any char / digit / word char / space |
[abc] / [^abc] / [a-z] |
one of / none of / a range |
^ / $ / \b |
start / end / word boundary |
* / + / ? |
0+ / 1+ / optional |
{2} / {2,5} / {2,} |
exactly / range / at least |
*? +? |
lazy (smallest match) |
(...) / \| |
group + capture / or |
\1 $1 |
back-reference to a group |
The trick isn’t memorising patterns — it’s reading them: scan left to right, naming each
piece (“start of line, one-or-more digits, a literal dash…”). Build that habit on real
grep and sed tasks and regex stops being line noise. A site like regex101.com is the
best place to test a pattern against real input as you go.
Get new field notes by email
Occasional, practical write-ups on building and shipping software. No spam — unsubscribe anytime.