Regex in Bash
Use the =~ operator, BASH_REMATCH, grep -E, and sed regex for practical pattern matching in Bash scripts.
The =~ Operator
Regular expressions let you validate and extract structured data from strings without writing custom parsing logic. Bash’s =~ operator inside [[ ]] tests whether a string matches a POSIX Extended Regular Expression, giving you the full power of regex in a native shell condition — no grep subprocess needed.
email="[email protected]"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
else
echo "Invalid email"
fi
# Store complex patterns in a variable to avoid quoting issues
# Never quote the variable when using it with =~ — quoting forces a literal match
ipv4_re='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
ip="192.168.1.100"
if [[ "$ip" =~ $ipv4_re ]]; then
echo "Looks like an IPv4 address"
fi
Key rule: do NOT quote the regex. [[ "$str" =~ "pattern" ]] does a literal string match, not regex. Assign the pattern to a variable and use that variable unquoted.
BASH_REMATCH — Capturing Groups
After a successful =~ match, Bash populates the BASH_REMATCH array with the captured groups. BASH_REMATCH[0] is the entire match; BASH_REMATCH[1] is the first capturing group, and so on. This lets you extract structured data from strings without calling awk or sed.
log_line="2025-01-15 14:32:07 ERROR database connection failed"
# Define the pattern with capturing groups for each field
re='^([0-9]{4}-[0-9]{2}-[0-9]{2}) ([0-9]{2}:[0-9]{2}:[0-9]{2}) ([A-Z]+) (.+)$'
if [[ "$log_line" =~ $re ]]; then
echo "Full match: ${BASH_REMATCH[0]}"
echo "Date: ${BASH_REMATCH[1]}" # 2025-01-15
echo "Time: ${BASH_REMATCH[2]}" # 14:32:07
echo "Level: ${BASH_REMATCH[3]}" # ERROR
echo "Message: ${BASH_REMATCH[4]}" # database connection failed
fi
Practical: parse a semantic version string into its components:
version="v2.14.3-beta"
re='^v([0-9]+)\.([0-9]+)\.([0-9]+)(-(.+))?$'
if [[ "$version" =~ $re ]]; then
major="${BASH_REMATCH[1]}" # 2
minor="${BASH_REMATCH[2]}" # 14
patch="${BASH_REMATCH[3]}" # 3
label="${BASH_REMATCH[5]}" # beta (group 4 is the "-beta" part, group 5 is just "beta")
echo "Major: $major, Minor: $minor, Patch: $patch, Label: ${label:-stable}"
fi
ERE Syntax Reference
Bash =~ uses POSIX Extended Regular Expressions. The key difference from Perl-compatible regex (PCRE) is that \d and \w are not supported — use character classes like [0-9] and [a-zA-Z0-9_] instead.
| Pattern | Meaning | Example |
|---|---|---|
. | Any single character | a.c matches abc, axc |
* | Zero or more of previous | ab*c matches ac, abc, abbc |
+ | One or more of previous | ab+c matches abc, abbc |
? | Zero or one of previous | colou?r matches color, colour |
{n} | Exactly n | [0-9]{4} matches 2025 |
{n,m} | Between n and m | [0-9]{2,4} |
^ | Start of line | ^root |
$ | End of line | \.sh$ |
[abc] | Character class | [aeiou] |
[^abc] | Negated class | [^0-9] non-digit |
(a|b) | Alternation | (cat|dog) |
() | Capturing group | ([0-9]+) |
\d | Digit (PCRE only — use [0-9] in ERE) | |
\w | Word char (PCRE only — use [a-zA-Z0-9_]) |
grep -E — Extended Regex on Files
grep -E applies extended regex to files and command output. It is the right tool when you need to filter or extract patterns from large text files — logs, configuration files, source code — without loading them into memory as variables.
# Match lines with extended regex — any of these patterns
grep -E "error|warning|critical" /var/log/app.log
# Match IP addresses in an access log
grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' access.log
# Extract only the matched part (not the whole line)
grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' access.log
# Find email addresses across all source files
grep -rhoE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' ./src/
# Validate a value in a pipeline — use exit code
echo "[email protected]" | grep -qE '^[^@]+@[^@]+\.[a-zA-Z]{2,}$' \
&& echo "valid" || echo "invalid"
grep -P — Perl-Compatible Regex
When you need lookaheads, lookbehinds, or non-greedy matching, grep -P provides PCRE. Note that -P is GNU grep only — not available on macOS’s BSD grep.
# Lookahead: find lines with "foo" where "bar" also appears later on the line
grep -P 'foo(?=.*bar)' file.txt
# Lookbehind: extract prices that follow a $ sign
grep -oP '(?<=\$)[0-9]+\.[0-9]{2}' file.txt
# Non-greedy match — match as little as possible
grep -oP '<title>.+?</title>' page.html
Note: -P is GNU grep only, not available on macOS’s BSD grep. Use brew install grep for ggrep -P on macOS.
sed with Regex
sed applies regex-based transformations to text. It is the right tool when you need to transform content — not just filter it. Capture groups let you rearrange parts of a match in the replacement.
# Basic substitution
sed 's/old/new/' file.txt
# Extended regex with -E (cleaner syntax)
sed -E 's/[0-9]{4}-[0-9]{2}-[0-9]{2}/REDACTED/g' log.txt
# Capture groups in the replacement — reference with \1, \2
echo "John Smith" | sed -E 's/([A-Z][a-z]+) ([A-Z][a-z]+)/\2, \1/'
# Smith, John
# Extract a specific part of each matching line
echo "error: line 42: undefined variable" | sed -E 's/.*line ([0-9]+).*/\1/'
# 42
# Delete lines matching a pattern
sed -E '/^[[:space:]]*#/d' config.txt # remove comment lines
sed -E '/^[[:space:]]*$/d' file.txt # remove blank lines
# Reformat dates from YYYY-MM-DD to DD/MM/YYYY
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/g' report.txt
Practical Patterns
Validate inputs in scripts
Input validation with regex prevents bad data from reaching the core logic of your script and produces clear error messages instead of confusing failures later.
validate_email() {
local re='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
[[ "$1" =~ $re ]]
}
validate_ipv4() {
# Each octet: 0-255
local re='^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
[[ "$1" =~ $re ]]
}
validate_semver() {
local re='^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'
[[ "$1" =~ $re ]]
}
validate_email "[email protected]" && echo "valid" || echo "invalid" # valid
validate_ipv4 "192.168.1.999" && echo "valid" || echo "invalid" # invalid
validate_semver "v2.1.0-beta" && echo "valid" || echo "invalid" # valid
Parse structured log lines
#!/usr/bin/env bash
# Parse nginx access log lines
# Format: IP - - [date] "METHOD /path HTTP/1.1" STATUS bytes
re='^([0-9.]+) - - \[([^\]]+)\] "([A-Z]+) ([^ ]+) HTTP/[0-9.]+" ([0-9]{3}) ([0-9]+)'
while IFS= read -r line; do
if [[ "$line" =~ $re ]]; then
ip="${BASH_REMATCH[1]}"
status="${BASH_REMATCH[5]}"
bytes="${BASH_REMATCH[6]}"
path="${BASH_REMATCH[4]}"
# Alert on server errors
if [[ "$status" =~ ^5 ]]; then
echo "5xx ERROR: $ip $path -> $status (${bytes} bytes)"
fi
fi
done < /var/log/nginx/access.log
Extract config values
# Extract value from KEY=VALUE or KEY: VALUE format config files
get_config_value() {
local file="$1"
local key="$2"
local re="^[[:space:]]*${key}[[:space:]]*[=:][[:space:]]*(.+)[[:space:]]*$"
local line
while IFS= read -r line; do
if [[ "$line" =~ $re ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
done < "$file"
return 1 # key not found
}
db_host=$(get_config_value /etc/myapp/config.ini "db_host")
echo "DB host: $db_host"
What’s Next
The next tutorial covers networking — using curl, wget, nc, ssh, scp, rsync, and writing basic port checks in Bash.