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
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
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 dot | file.txt, 192.168.1.1 |
\* | A literal asterisk | 5 * 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 backslash | C:\Users\ |
\n | Newline (in GNU tools) | Embedded newline in pattern space |
\t | Tab 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.
# 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
Escaping and Quoting
^ (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.