Regular Expressions in Bash
Chapter 3 — Anchors: Start, End, and Word Boundaries
What Are Anchors?
An anchor does not match a character — it matches a position in the text. The two most important anchors, ^ and $, pin your pattern to the start or end of a line. Without anchors, a pattern can match anywhere on the line; with anchors, you specify exactly where it must appear.
Think of anchors as invisible markers that say: "the match must begin here" or "the match must end here".
\w) and a non-word character (\W) — i.e. the edge of a word. Matches at both the start and end of words.
Works in: GNU grep, GNU sed, GNU awk
\< matches the position at the start of a word; \> matches the position at the end. Together they are the portable alternative to \b.
Works in: grep, sed (POSIX BRE/ERE)
^ — The Start-of-Line Anchor
^ asserts that what follows must appear at the very beginning of the line. Without ^, the pattern can match anywhere; with ^, it must be the first thing on the line.
# Lines that start with "Error" grep '^Error' app.log # Lines that start with a digit grep '^[0-9]' data.txt # Lines that start with optional whitespace then a # (comment lines) grep '^[[:space:]]*#' config.conf # Delete all comment lines from a config file sed '/^[[:space:]]*#/d' config.conf # In awk: process lines that start with "WARN" awk '/^WARN/ { print NR": "$0 }' app.log # In [[ =~ ]]: does this string start with a digit? [[ "$line" =~ ^[0-9] ]] && echo "starts with digit"
^ inside a character class means negation — not an anchor
# ^ at the START of the pattern = start-of-line anchor grep '^cat' # line must START with "cat" # ^ as the FIRST char inside [...] = negation (match anything NOT in set) grep '[^abc]' # match any character that is NOT a, b, or c # ^ anywhere else inside [...] = literal caret character grep '[a^b]' # matches a, ^, or b — ^ is literal here
^ has two completely different meanings depending on context. At the start of a pattern or after | it is a start-of-line anchor. As the first character inside [...] it negates the class. Everywhere else inside [...] it is just a literal caret. Memorise this — it trips up everyone.
$ — The End-of-Line Anchor
$ asserts that what precedes must appear at the very end of the line. The regex engine matches $ against the position just before the newline character (which SED and grep strip off before processing).
# Lines that end with a semicolon grep ';$' source.js # List only .log files ls | grep '\.log$' # Lines that end with one or more digits grep -E '[0-9]+$' data.txt # Remove trailing whitespace (common pre-commit fix) sed 's/[[:space:]]*$//' file.txt # Delete lines that end with a backslash (continuation lines) grep -v '\\$' makefile # \\ matches literal \, then $ anchors to end
Combining ^ and $ Together
When you use ^ and $ together, you anchor the pattern to the entire line — the match must cover the line from start to finish.
# Match blank lines (nothing between start and end) grep '^$' file.txt # exactly empty grep '^[[:space:]]*$' file.txt # blank or whitespace-only # Delete blank lines sed '/^$/d' file.txt # Match lines that contain ONLY digits (nothing else) grep -E '^[0-9]+$' data.txt # Validate that a variable is a positive integer [[ "$val" =~ ^[0-9]+$ ]] && echo "valid integer" # Match lines that are exactly 8 characters long grep -E '^.{8}$' file.txt # Match lines containing ONLY uppercase letters and spaces grep -E '^[A-Z ]+$' file.txt # In sed: add text at the very start of every line sed 's/^/ /' file.txt # indent every line by 4 spaces # In sed: add text at the very end of every line sed 's/$/;/' file.txt # append semicolon to every line
Word Boundaries
Line anchors pin you to the start or end of the entire line. But often you want to match a word only when it appears as a complete word — not as part of a longer word. Word boundary anchors solve this.
\b — word boundary (GNU)
A word character is any letter, digit, or underscore ([[:alnum:]_]). A word boundary is the position between a word character and a non-word character (or between a word character and the start/end of the line).
# Match the word "cat" but not "concatenate" or "scat" echo "the cat concatenate scat scatter" | grep -oE '\bcat\b' cat # Replace the word "error" but not "errors" or "no_error_code" sed 's/\berror\b/ERROR/g' app.log # Count occurrences of the exact word "the" grep -oE '\bthe\b' essay.txt | wc -l # \b also anchors to start/end of line echo "error at line 5" | grep -E '\berror\b' error at line 5 # matches — "error" is a standalone word at line start
\B — non-word boundary (GNU)
\B is the opposite of \b — it matches a position that is not at a word boundary, i.e. the middle of a word.
# Match "cat" only when it is part of a longer word echo "cat concatenate scat" | grep -oE '\Bcat\B' cat # only the "cat" inside "con-cat-enate" matches # Practical: find compound words containing "data" grep -E '\Bdata\B' source.py # matches metadata, database but not standalone "data"
\< and \> — POSIX word edges
These are the POSIX-portable way to match word boundaries. \< matches the start of a word; \> matches the end. They work in GNU grep and sed in BRE mode without -E, making them the safest choice in portable scripts.
# Match whole word "cat" using POSIX word boundaries grep '\<cat\>' file.txt # Same as \bcat\b but more portable echo "the cat concatenate scat" | grep -o '\<cat\>' cat # In sed: replace whole word only sed 's/\<error\>/ERROR/g' app.log # Only word-start boundary (match words beginning with "pre") grep '\<pre' dictionary.txt # preview, prepare, prefix — but not "impressive" # Only word-end boundary (match words ending in "ing") grep 'ing\>' text.txt # running, jumping — but not "rings"
Anchor Portability — What Works Where
| Anchor | grep (BRE) | grep -E (ERE) | grep -P (PCRE) | sed | awk | [[ =~ ]] |
|---|---|---|---|---|---|---|
^ |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
$ |
✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
\b |
GNU only | GNU only | ✓ | GNU only | GNU awk | GNU bash |
\B |
GNU only | GNU only | ✓ | GNU only | GNU awk | GNU bash |
\< \> |
✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
\< and \> when writing scripts for POSIX environments or macOS (BSD tools). Use \b freely in interactive one-liners on Linux where GNU tools are guaranteed. In [[ =~ ]] and awk, use character class workarounds if portability matters.
Practical Anchor Patterns
Input validation in scripts
# Is input a valid positive integer? is_integer() { [[ "$1" =~ ^[0-9]+$ ]] } is_integer "42" && echo "ok" # ok is_integer "3.14" && echo "ok" # no match — dot disqualifies it is_integer "12x" && echo "ok" # no match — trailing x # Is input a valid ISO date (YYYY-MM-DD)? is_date() { [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] } is_date "2026-06-11" && echo "valid" is_date "06/11/2026" && echo "valid" # no match — wrong format # Does the string start with http:// or https://? [[ "$url" =~ ^https?:// ]] || echo "not a URL"
Config and log file tasks
# Print only non-blank, non-comment lines from a config file grep -Ev '^[[:space:]]*(#|$)' sshd_config # Find lines where a key appears at the start (key=value format) grep '^MaxSessions' /etc/ssh/sshd_config # Find duplicate words (same word at end and start of adjacent lines) grep -E '\bthe\b.*\bthe\b' essay.txt # "the" twice on same line # Show only lines that are pure section headers [SectionName] grep -E '^[[:space:]]*\[[[:alnum:]_]+\][[:space:]]*$' app.conf # Add a line number prefix only to non-blank lines grep -n '.' file.txt # . matches any char = any non-empty line
Whole-word replacement in sed
# Replace standalone "cat" — not "cats", "tomcat", "concatenate" sed 's/\bcat\b/dog/g' file.txt # GNU sed sed 's/\<cat\>/dog/g' file.txt # POSIX portable # Rename a function across a codebase (word boundary prevents partial matches) sed -i 's/\bgetUser\b/fetchUser/g' *.js # Replace a word at the start of a line only sed 's/^TODO/DONE/' tasks.txt # Append a comma to every line that doesn't already end with one sed '/,$/! s/$/,/' list.txt # /,$/! = lines NOT ending in comma
Common Anchor Mistakes
| Mistake | Pattern used | Problem | Fix |
|---|---|---|---|
| Matching anywhere instead of start | grep 'Error' |
Matches "Error" mid-line too | grep '^Error' |
| Trailing space breaks end anchor | grep '\.log$' |
Fails if line has trailing space | grep '\.log[[:space:]]*$' |
| Partial word match | sed 's/cat/dog/g' |
Changes "concatenate" → "condogenate" | sed 's/\bcat\b/dog/g' |
^ inside class negates instead of anchors |
grep '[^a-z]' |
Matches non-lowercase chars, not start of line | Use ^[a-z] outside brackets for anchoring |
Using \b on macOS/BSD |
grep '\bword\b' |
BSD grep doesn't support \b |
grep '\<word\>' or grep -E '\bword\b' with GNU grep |
Forgetting $ in validation |
[[ "$v" =~ ^[0-9]+ ]] |
"123abc" passes — only checks the start | [[ "$v" =~ ^[0-9]+$ ]] |
Quick Reference — Chapter 3
Line Anchors
Word Boundaries
[abc], [a-z], negated classes [^...], and the full set of POSIX named classes like [[:alpha:]], [[:digit:]], and [[:space:]]. Character classes give you precise control over which characters are allowed at any position in your pattern.