Regular Expressions in Bash

Chapter 2 — Literals, the Dot, and the Escape

Literal Characters

The simplest possible regex is a sequence of literal characters — characters that match themselves exactly. Every letter, digit, and most punctuation marks are literals. The engine looks for that exact sequence anywhere in the input line.

# A literal pattern matches anywhere on the line
echo -e "apple\napplesauce\npineapple\nbanana" | grep 'apple'
apple
applesauce
pineapple
# "banana" has no match — 'apple' must appear somewhere on the line

# Matching is case-sensitive by default
echo -e "Error\nerror\nERROR" | grep 'error'
error   # only the exact lowercase version matches

# Use -i for case-insensitive matching
echo -e "Error\nerror\nERROR" | grep -i 'error'
Error
error
ERROR
Pattern: 'cat' — where does it match in each line? cat → match at position 0 concatenate → match at position 3 the cat sat → match at position 4 dog → no match

Digits and spaces are literals too

# Digits match themselves
grep '404' access.log           # lines containing "404"
grep '2026' dates.txt            # lines containing "2026"

# A space is a literal space
grep 'hello world' file.txt      # the space must be there

# Multiple words: the whole string is one pattern
grep 'connection refused' syslog # all three words in sequence

The Dot — Matching Any Single Character

The dot . is the first metacharacter most people learn. It matches any single character except a newline. Think of it as a wildcard slot: put a . where any one character is acceptable.

# . matches exactly one character — any character
echo -e "cat\nbat\nhat\nsat\nat" | grep '.at'
cat
bat
hat
sat
# "at" alone has no match — . requires exactly one character before "at"

# Two dots = two characters (any)
echo -e "a1b\naxb\na b\nabc" | grep 'a.b'
a1b
axb
a b
# "abc" has no match — . requires exactly one char between a and b

# .* means "any number of any characters" (very common pattern)
echo -e "start-middle-end\nstart-end\nstart123end" | grep 'start.*end'
start-middle-end
start-end
start123end
Pattern: c.t grep 'c.t' <<< "word" cat ✓ (c + a + t) cut ✓ (c + u + t) c9t ✓ (c + 9 + t) c t ✓ (c + space + t) ct ✗ (nothing between c and t) coat ✗ (two chars between c and t)
Pattern: .... grep -E '^....$' (exactly 4 chars) cats ✓ (4 characters) 1234 ✓ (4 characters) r!@# ✓ (4 characters) cat ✗ (3 characters) hello ✗ (5 characters) ^ and $ anchor to full line

The dot trap — matching when you mean a literal period

Because . matches any character, it will silently match things you didn't intend. This is the most common regex mistake in Bash scripts:

# Intended: match version "1.0" specifically
echo -e "v1.0\nv1x0\nv100" | grep '1.0'
v1.0
v1x0   # oops — . matched x

# Fix: escape the dot with \
echo -e "v1.0\nv1x0\nv100" | grep '1\.0'
v1.0   # only the real version matches now

# Real-world example: matching an IP address pattern safely
echo -e "192.168.1.1\n192X168X1X1" | grep '192\.168\.'
192.168.1.1   # only the real IP

# Matching a file extension: .txt must be a literal dot
ls | grep '\.txt$'   # ends with a literal .txt

The Backslash Escape

The backslash \ removes the special meaning from the character that follows it. After a backslash, a metacharacter becomes a literal. This is called escaping.

You write Matches Example input that matches
\.A literal dotfile.txt, 192.168.1.1
\*A literal asterisk5 * 3, footnote*
\+A literal plus sign (in BRE)a+b, C++
\?A literal question mark (in BRE)why?
\^A literal caret^start, a^b
\$A literal dollar sign$100, $VAR
\[A literal opening bracket[note]
\(A literal opening paren (in ERE)(optional)
\|A literal pipe (in ERE)a|b
\\A literal backslashC:\Users\
\nNewline (in GNU tools)Embedded newline in pattern space
\tTab character (in GNU tools)Tab-separated data
# Escaping a dollar sign (common in shell scripts)
echo 'Price: $99.50' | grep '\$[0-9]'
Price: $99.50

# Matching a literal asterisk in text
echo -e "5 * 3 = 15\n5 x 3 = 15" | grep '\*'
5 * 3 = 15

# Matching a Windows-style path (literal backslashes)
echo 'C:\Users\emuba\file.txt' | grep 'Users\\emuba'
C:\Users\emuba\file.txt

# Matching a literal question mark
echo -e "Are you sure?\nAre you sure" | grep 'sure\?$'
Are you sure?
# In BRE: \? is a literal ?  — in ERE: \? means "zero or one"

The Double-Escape Problem — Shell Quoting and Regex

Here is where many Bash users get confused. When you write a regex on the command line, there are two layers of interpretation: first the shell processes the quoting, then the regex engine processes what remains.

Two layers: shell first, then regex engine Command you type: grep '192\.168' file Shell sees: single quotes — passes everything through unchanged Regex engine gets: 192\.168 → matches 192.168   Command you type: grep "192\\.168" file Shell sees: double quotes — \\ becomes \ Regex engine gets: 192\.168 → same result   Command you type: grep "192\.168" file Shell sees: double quotes — \. stays as \. (\ not special before .) Regex engine gets: 192\.168 → same result (this one usually works)
Use single quotes for regex patterns. Single quotes prevent the shell from interpreting anything — no variable expansion, no backslash processing. The pattern reaches the regex engine exactly as written. Only switch to double quotes when you need to expand a shell variable inside the pattern.
# Single quotes: shell passes everything through literally (preferred)
grep '\$[0-9]\+' file.txt       # shell does nothing; engine sees \$[0-9]\+

# Double quotes: shell processes \\ → \ before the engine sees it
grep "\\$[0-9]\\+" file.txt     # shell: \\ → \, then \$ → $, then \+ → \+... tricky

# Double quotes needed when expanding a variable in the pattern
TERM="error"
grep "$TERM" app.log             # $TERM expands to "error" — double quotes needed

# Variable in pattern with special chars: escape the variable content
TERM="v1.0"
# BAD: . inside $TERM is treated as regex metachar
grep "$TERM" file.txt            # matches v1x0, v1.0, v100 etc.
# BETTER: use grep -F for literal string search
grep -F "$TERM" file.txt         # -F = fixed string, no regex

grep -F — When You Don't Want Regex At All

Sometimes you have a search term that contains special characters and you want to match it literally — no regex processing at all. The -F flag (fixed string) tells grep to treat the entire pattern as plain text.

# Searching for a URL — slashes, dots, question marks all literal
grep -F 'https://example.com/api?v=2' access.log

# Searching for a literal asterisk or plus sign
grep -F 'a+b=c' math.txt

# Searching for a pattern that came from a variable (safest approach)
SEARCH="$100.00"
grep -F "$SEARCH" invoice.txt    # $ and . treated as literals

# -F is also significantly faster on large files
# because no regex engine compilation is needed
grep -F "Connection refused" /var/log/syslog

Case Sensitivity

Regex is case-sensitive by default in all Bash tools. There are two ways to match regardless of case:

# -i flag: case-insensitive for grep
grep -i 'error' app.log          # matches Error, ERROR, error, ErRoR

# -i flag: case-insensitive for sed (GNU sed)
sed 's/error/FOUND/Ig' app.log   # I flag on s command

# In regex: use a character class to be explicit about what you allow
grep '[Ee]rror' app.log          # matches Error or error (not eRror)
grep '[Ee][Rr][Rr][Oo][Rr]' app.log  # matches any capitalisation

# In [[ =~ ]]: no -i flag — use character classes
word="ERROR"
[[ "$word" =~ [Ee][Rr][Rr][Oo][Rr] ]] && echo "matched"

Practical Patterns — Putting It Together

Matching version numbers

# Match "1.2.3" style version strings — escape all dots
grep '[0-9]\+\.[0-9]\+\.[0-9]\+' changelog.txt        # BRE
grep -E '[0-9]+\.[0-9]+\.[0-9]+' changelog.txt        # ERE

# Match a specific version literally
grep -F '2.14.1' changelog.txt

Matching file extensions

# Files ending in .log (literal dot, then log, then end of line)
ls | grep '\.log$'

# Files ending in .tar.gz (two literal dots)
ls | grep '\.tar\.gz$'

# Any hidden file (starts with a dot)
ls -a | grep '^\..'   # ^ then literal dot then any char (at least 2 char name)

Matching decimal numbers

# Match a decimal number like 3.14, 99.99, 0.5
grep -E '[0-9]+\.[0-9]+' data.txt

# Match in a sed substitution
sed -E 's/[0-9]+\.[0-9]+/PRICE/g' invoice.txt

# Test in [[ =~ ]] — is this string a decimal number?
val="3.14"
[[ "$val" =~ ^[0-9]+\.[0-9]+$ ]] && echo "is decimal"
is decimal

Common traps and fixes

Trap Wrong pattern Fixed pattern Why
Unescaped dot in version grep '2.0' grep '2\.0' . matches any char
Dot in domain name grep 'example.com' grep 'example\.com' Matches exampleXcom too
Dollar sign in price grep '$99' grep '\$99' or grep -F '$99' $ is an anchor
Asterisk in maths expression grep '3*4' grep '3\*4' or grep -F '3*4' * is a quantifier
Dot in file extension check grep '.txt$' grep '\.txt$' Matches atxt 1txt etc.

Quick Reference — Chapter 2

Literals and the Dot

a b c 0 9 space All letters, digits, and spaces are literals — match themselves
. Matches any single character except newline
\. Matches a literal dot (escaped metacharacter)
.* Matches zero or more of any character (greedy)

Escaping and Quoting

\ Escape — removes special meaning from the next character
'pattern' Single quotes — shell passes pattern through unchanged (preferred)
"$VAR" Double quotes — use when you need variable expansion in the pattern
grep -F 'literal' Fixed string — no regex processing at all
grep -i 'pattern' Case-insensitive match
What is coming next: Chapter 3 covers anchors — ^ (start of line), $ (end of line), and word boundaries (\b, \<, \>). Anchors let you pin your pattern to a specific position in the line rather than matching it anywhere.