03 · Regular Expressions in Bash¶
Regular expressions let you match patterns in text rather than exact strings — essential for validating input, filtering logs, and extracting structured data out of otherwise messy text.
grep -E — extended regular expressions¶
echo "error: connection timed out" | grep -E "error|warning"
# error: connection timed out (matches "error" OR "warning")
echo "user123" | grep -E "^[a-z]+[0-9]+$"
# user123 (letters followed by digits, whole string)
# common quantifiers
grep -E "colou?r" file.txt # ? = 0 or 1 of the preceding token: color OR colour
grep -E "ab+c" file.txt # + = 1 or more: abc, abbc, abbbc...
grep -E "ab*c" file.txt # * = 0 or more: ac, abc, abbc...
grep -E "a{2,4}" file.txt # {2,4} = between 2 and 4 repetitions
Without -E, grep uses "basic" regex where +, ?, |, and {} need a
backslash to be special (grep "ab\+c") — -E (or the egrep alias) gives
you the more readable "extended" syntax used throughout this module.
Anchors and character classes¶
grep -E "^#" script.sh # lines starting with # (comments)
grep -E ";$" script.sh # lines ending with ;
grep -E "^\s*$" file.txt # blank or whitespace-only lines
grep -E "[0-9]{3}-[0-9]{4}" contacts.txt # phone-number-shaped patterns like 555-1234
grep -E "[[:alpha:]]+" file.txt # POSIX class: alphabetic characters
grep -E "[[:digit:]]+" file.txt # POSIX class: digits
| Class | Meaning |
|---|---|
^ |
start of line |
$ |
end of line |
. |
any single character |
[abc] |
any one of a, b, c |
[^abc] |
any character except a, b, c |
[[:alpha:]] |
any letter |
[[:digit:]] |
any digit |
[[:space:]] |
any whitespace |
The =~ operator — regex matching inside [[ ]]¶
input="user@example.com"
if [[ "$input" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "valid email format"
else
echo "invalid email format"
fi
=~ lets you regex-match directly in a conditional, no external grep
process needed — the pattern on the right side should be unquoted (or
bash treats it as a literal string instead of a regex).
Capture groups with =~ and BASH_REMATCH¶
log_line="2026-07-18 14:32:01 ERROR Connection refused"
if [[ "$log_line" =~ ^([0-9-]+)\ ([0-9:]+)\ ([A-Z]+)\ (.*)$ ]]; then
echo "date: ${BASH_REMATCH[1]}"
echo "time: ${BASH_REMATCH[2]}"
echo "level: ${BASH_REMATCH[3]}"
echo "message: ${BASH_REMATCH[4]}"
fi
# date: 2026-07-18
# time: 14:32:01
# level: ERROR
# message: Connection refused
BASH_REMATCH[0] holds the whole match; BASH_REMATCH[1], [2], ... hold
each parenthesized capture group, in order — this is the cleanest way to
pull structured fields out of a line of text without spawning awk.
grep capture groups with -o and -E¶
echo "Contact: alice@example.com" | grep -oE "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"
# alice@example.com
-o prints only the matched portion of each line instead of the whole line
— handy for extracting IDs, IPs, or emails out of larger text.
Practical validation patterns¶
is_number() { [[ "$1" =~ ^-?[0-9]+$ ]]; }
is_ipv4() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; }
is_hex_color() { [[ "$1" =~ ^#[0-9a-fA-F]{6}$ ]]; }
is_number "-42" && echo "valid integer"
is_ipv4 "192.168.1.1" && echo "looks like an IPv4 address"
is_hex_color "#3a7bd5" && echo "valid hex color"
Note is_ipv4 here only checks shape, not that each octet is <= 255 —
real validation would combine the regex with a numeric range check per
octet.
How It Actually Works¶
[[ $str =~ $pattern ]] compiles $pattern using the C library's POSIX
extended regular expression engine (regcomp(3)/regexec(3)), the same
regex engine glibc exposes to any C program — bash itself contains no regex
matcher of its own. This is why bash's =~ supports ERE syntax
(+, ?, |, {n,m}) but not Perl-style features like non-greedy
quantifiers or lookahead: those aren't in POSIX ERE, so the underlying
regcomp call would reject them.
On a successful =~ match, bash populates the BASH_REMATCH array
directly from the regmatch_t capture-group offsets that regexec returns
— BASH_REMATCH[0] is the whole match, [1] is the first parenthesized
group, and so on, all computed as byte offsets into the original string and
then sliced out by bash after the C call returns.
grep -E and sed -E invoke their own, separate regex implementations as
independent processes — this matters because grep's ERE dialect can differ
subtly from glibc's regcomp (BRE vs ERE handling of escapes, POSIX
character classes like [[:alpha:]]), so a pattern that matches under =~
isn't guaranteed to match identically when piped through grep -E, and vice
versa, even though both claim "extended regular expressions."
Cheat sheet¶
| Pattern | Matches |
|---|---|
^ / $ |
start / end of line |
. |
any character |
* / + / ? |
0+, 1+, 0-or-1 of the preceding token |
{n,m} |
between n and m repetitions |
[...] / [^...] |
a character class / its negation |
(...) |
a capture group |
a\|b |
alternation: a OR b |
[[ $s =~ regex ]] |
match $s against regex, populate BASH_REMATCH |
Exercise¶
Write validate_log.sh that reads a log file line by line, uses =~ with a
capture-group regex to extract the timestamp, level, and message from lines
shaped like 2026-07-18 14:32:01 ERROR Connection refused, and prints only
the lines where the level is ERROR or CRITICAL, reformatted as
[LEVEL] message (at timestamp).