Skip to main content
Bash intermediate Lesson 15 of 22

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.

PatternMeaningExample
.Any single charactera.c matches abc, axc
*Zero or more of previousab*c matches ac, abc, abbc
+One or more of previousab+c matches abc, abbc
?Zero or one of previouscolou?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]+)
\dDigit (PCRE only — use [0-9] in ERE)
\wWord 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.

Frequently Asked Questions

What regex flavor does Bash use with =~?
Bash uses POSIX Extended Regular Expressions (ERE) for the =~ operator, the same flavor as grep -E. It does not support Perl-compatible regex (PCRE) features like lookaheads or non-greedy quantifiers.
How do I extract a captured group from a regex match?
After a successful =~ match, the array BASH_REMATCH holds the captures. BASH_REMATCH[0] is the full match, BASH_REMATCH[1] is the first group, and so on.
Should I quote the regex in [[ $var =~ regex ]]?
No. Do NOT quote the regex in [[ =~ ]]. Quoting forces a literal string match instead of regex interpretation. If your pattern is complex, assign it to a variable first: re='pattern'; [[ $var =~ $re ]].