Regular Expressions in Bash

Chapter 7 — grep and egrep in Depth

grep at a Glance

grepGlobal Regular Expression Print — is the primary regex tool at the Bash command line. It reads lines from files or stdin, tests each line against a pattern, and by default prints lines that match. It is fast, composable, and available on every Unix system.

Understanding grep's flags is what separates one-line lookups from powerful search pipelines. This chapter covers every flag you will reach for regularly, how to combine them, and when to switch between the three grep modes: BRE, ERE, and PCRE.

The Three grep Modes

grep / grep -G BRE — Basic Regex Default mode. + ? ( | are literals; need \+ \? \( \|. Portable to all Unix systems.
grep -E / egrep ERE — Extended Regex Cleaner syntax. + ? ( | work without backslashes. Recommended for everyday use.
grep -P PCRE — Perl Compatible Full Perl regex: lookahead, lookbehind, lazy .*?, named groups. GNU grep only — not on macOS BSD grep.
# BRE: one-or-more needs \+
grep '[0-9]\+' file.txt

# ERE: cleaner — + works directly
grep -E '[0-9]+' file.txt
egrep '[0-9]+' file.txt   # egrep is an alias for grep -E

# PCRE: lazy quantifier — not available in BRE/ERE
grep -P '<.+?>' file.html
egrep is deprecated in POSIX but still widely available as a symlink to grep -E. Prefer grep -E in scripts for clarity and portability.

Essential Output Flags

-o Only matching Print only the matched text, not the whole line. Each match on its own line.
-n Line numbers Prefix each matching line with its line number in the file.
-c Count Print a count of matching lines instead of the lines themselves.
-l Files with matches Print only the filename of files that contain at least one match.
-L Files without matches Print only filenames of files that contain NO matches — the inverse of -l.
-v Invert match Print lines that do NOT match the pattern — every non-matching line.
-i Case insensitive Match regardless of letter case. ERROR, Error, error all match 'error'.
-w Whole word Match only whole words — equivalent to wrapping the pattern in \b...\b.
-x Whole line Match only lines where the entire line matches — equivalent to ^pattern$.
Input: three lines — "error: port 8080", "warning: low memory", "error: disk full" grep 'error'error: port 8080 / error: disk full (whole matching lines) grep -o 'error'error / error (just the matched text) grep -c 'error'2 (count of matching lines) grep -n 'error'1:error: port 8080 / 3:error: disk full grep -v 'error'warning: low memory (non-matching line)
# -o: extract only the matched portion — ideal for data extraction
echo "order #12345 placed on 2026-06-11" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}'
2026-06-11

# -o with multiple matches per line: each on its own line
echo "call 555-1234 or 555-5678" | grep -oE '[0-9]{3}-[0-9]{4}'
555-1234
555-5678

# -c: count matching lines (not individual matches)
grep -c 'ERROR' app.log
47

# -l: which files in a directory contain a pattern?
grep -rl 'TODO' src/

# -L: which files do NOT contain a shebang?
grep -rL '^#!' bin/

# -w: whole word — "error" won't match "errors" or "no_error_code"
grep -w 'error' app.log

# -x: only lines that are EXACTLY this string
grep -x 'done' status.txt   # "done" alone on a line — not "done." or " done"

# -i: case-insensitive (useful for logs with mixed conventions)
grep -i 'warning' app.log    # matches WARNING, Warning, warning

Context Flags — Seeing Surrounding Lines

Context flags show lines before and/or after each match — invaluable when debugging log files or reading code with grep.

grep -B2 -A2 'ERROR' — 2 lines before and after each match 14-2026-06-11 14:31:58 INFO Processing batch 7 15-2026-06-11 14:31:59 INFO Connecting to database 16:2026-06-11 14:32:00 ERROR Connection timeout after 30s 17-2026-06-11 14:32:01 INFO Retrying connection (1/3) 18-2026-06-11 14:32:04 INFO Retrying connection (2/3) -- (separator between match groups)
# -A n: show n lines After each match
grep -A3 'ERROR' app.log

# -B n: show n lines Before each match
grep -B2 'ERROR' app.log

# -C n: show n lines of Context (before AND after)
grep -C3 'ERROR' app.log

# Context with line numbers — very useful for finding the exact source location
grep -n -C2 'def process' module.py

# Groups separated by "--" — suppress the separator
grep -A2 --no-group-separator 'ERROR' app.log

File and Directory Flags

# -r: recursive search through directories
grep -r 'api_key' /etc/

# -R: recursive, but also follows symlinks (GNU grep)
grep -R 'TODO' src/

# --include: only search files matching a glob
grep -r --include='*.py' 'import os' .
grep -r --include='*.{js,ts}' 'console\.log' src/

# --exclude: skip files matching a glob
grep -r --exclude='*.min.js' 'function' .

# --exclude-dir: skip entire directories
grep -r --exclude-dir='.git' --exclude-dir='node_modules' 'password' .

# Combine -rl with --include for scoped file search
grep -rl --include='*.conf' 'MaxConnections' /etc/

# Search a list of files from another command
find . -name '*.log' -newer deploy.txt | xargs grep -l 'ERROR'

Multiple Patterns — -e and -f

# -e: specify multiple patterns (OR logic — line matches if any pattern matches)
grep -e 'ERROR' -e 'FATAL' -e 'CRITICAL' app.log
# Equivalent to: grep -E 'ERROR|FATAL|CRITICAL' app.log

# -f: read patterns from a file (one pattern per line)
cat patterns.txt
ERROR
FATAL
CRITICAL
grep -f patterns.txt app.log

# Combine -f with other flags
grep -if patterns.txt app.log   # -i = case insensitive
grep -cf patterns.txt app.log   # -c = count matching lines

# AND logic: pipe two greps (both patterns must match)
grep 'ERROR' app.log | grep 'database'  # lines with both ERROR and database

# NOT logic: grep -v to exclude a pattern
grep 'ERROR' app.log | grep -v 'connection'  # ERROR lines NOT about connections

grep -F — Fixed String (No Regex)

grep -F (also fgrep) treats the pattern as a plain string — no regex processing at all. It is significantly faster on large files because it uses the Boyer-Moore-Horspool string search algorithm instead of a regex engine.

# -F: safe for patterns that contain regex metacharacters
grep -F '192.168.1.1' access.log   # dots are literals, not any-char
grep -F 'a+b=c' math.txt          # + is literal
grep -F '$100.00' invoice.txt      # $ is literal
grep -F '[::1]' access.log         # brackets are literal

# -F with a variable (the safest pattern when content is unknown)
SEARCH="$USER_INPUT"
grep -F "$SEARCH" file.txt

# -F is much faster on large files — no regex compilation overhead
grep -F "Connection refused" /var/log/syslog   # faster than grep -E

# -F with -f: search for many fixed strings at once (very efficient)
grep -Ff known_ips.txt access.log   # match any of 10,000 IPs

grep Exit Codes — Using grep in Scripts

grep's exit code is what makes it useful inside shell scripts — not just for output, but as a conditional test.

0 Match found At least one line matched the pattern
1 No match Pattern was valid but no lines matched
2 Error Bad pattern syntax or file not found
# Use grep as a boolean test — -q suppresses all output
if grep -q 'ERROR' app.log; then
    echo "Errors found — alerting team"
fi

# -q (quiet): suppress output, just set exit code
# This is the standard idiom for "does this file contain X?"
grep -q 'pattern' file && echo "found" || echo "not found"

# Check if a process is running
if grep -q 'nginx' <(ps aux); then
    echo "nginx is running"
fi

# Validate that a config file has a required setting
if ! grep -qE '^MaxSessions[[:space:]]' /etc/ssh/sshd_config; then
    echo "WARNING: MaxSessions not configured" >&2
fi

# Count errors and fail if threshold exceeded
errors=$(grep -c 'ERROR' app.log)
((errors > 100)) && { echo "Too many errors: $errors"; exit 1; }

Practical Pipeline Patterns

Log analysis

# Frequency count of log levels
grep -oE '(ERROR|WARN|INFO|DEBUG)' app.log | sort | uniq -c | sort -rn
    523 INFO
     47 ERROR
     12 WARN
      3 DEBUG

# Top 10 most frequent IP addresses in an access log
grep -oE '^[0-9.]+' access.log | sort | uniq -c | sort -rn | head -10

# All unique HTTP status codes in today's log
grep -oE '" [0-9]{3} ' access.log | grep -oE '[0-9]{3}' | sort -u

# Extract all unique email addresses from a mailbox dump
grep -oE '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}' mail.txt | sort -u

# Show only error lines WITH their timestamp (context aware)
grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}.*ERROR' app.log

Code search

# Find all TODO/FIXME/HACK comments in a codebase
grep -rn --include='*.py' -E '#\s*(TODO|FIXME|HACK|XXX)' .

# Find function definitions in Python files
grep -rn --include='*.py' -E '^[[:space:]]*(def|async def) [[:alnum:]_]+' .

# Find hardcoded passwords or secrets (simple heuristic)
grep -rniE '(password|secret|api_key|token)\s*=\s*["\x27][^\s"]+["\x27]' . \
    --exclude-dir='.git' --exclude-dir='node_modules'

# Find all files that import a specific module
grep -rl --include='*.py' '^import os' .

# List all unique function names defined in a file
grep -oE '^def [[:alnum:]_]+' module.py | cut -d' ' -f2 | sort

Security and compliance checks

# Find files containing private key headers
grep -rl 'BEGIN.*PRIVATE KEY' /var/www/

# Check sshd_config for insecure settings
grep -E '^(PermitRootLogin yes|PasswordAuthentication yes)' /etc/ssh/sshd_config

# Audit: find world-writable files listed in a manifest
grep -oP '(?<=path=")[^"]+' manifest.xml | xargs ls -la | grep '^......rw'

grep -P Highlights — PCRE-Specific Power

When -E isn't enough, grep -P unlocks PCRE features. Full PCRE coverage is in Chapter 11; here are the patterns worth knowing now:

# Lazy quantifier: match each HTML tag individually
grep -oP '<.+?>' file.html

# Lookahead: match a word followed by a colon (but don't include the colon)
grep -oP '\w+(?=:)' file.txt

# Lookbehind: match digits that follow a $ sign
grep -oP '(?<=\$)[0-9]+' invoice.txt

# Named group: extract the value of a specific JSON field
grep -oP '"user":\s*"(?P<name>[^"]+)"' data.json

# Non-greedy: match content between the FIRST pair of matching delimiters
grep -oP '\[.+?\]' file.txt
grep -P is GNU grep only — it is not available on macOS (BSD grep) or strictly POSIX environments. Scripts that must run on macOS should use -E with workarounds, or call perl -ne for PCRE patterns.

Flag Combinations Cheat Sheet

Combination What it does Typical use
grep -qSilent test — exit code onlyScript conditionals
grep -cCount of matching linesMetrics, thresholds
grep -nLine numbers on outputDebugging, navigation
grep -oOnly the matched textData extraction
grep -oEExtract matches with EREParsing structured text
grep -vInvert — non-matching linesFiltering out noise
grep -iCase insensitiveMixed-case logs
grep -wWhole word matchAvoid partial matches
grep -rnRecursive with line numbersCodebase search
grep -rlRecursive, filenames onlyFind which files contain X
grep -rLRecursive, files without matchFind files missing a header
grep -C33 lines of contextLog debugging
grep -inECase-insensitive ERE with line numbersGeneral search
grep -FxqExact line, fixed string, quietCheck if list contains item
grep -oPExtract with PCREComplex extraction

Quick Reference — Chapter 7

Mode Flags

grep (no flag) BRE — Basic Regular Expressions (default)
grep -E / egrep ERE — Extended Regular Expressions (recommended)
grep -F / fgrep Fixed string — no regex, fastest option
grep -P PCRE — lookahead, lookbehind, lazy (GNU only)

Most-Used Flag Combinations

grep -q 'pat' file Silent boolean test — use in if statements
grep -oE 'pat' file Extract matching text only (one match per line)
grep -rn 'pat' dir/ Recursive search with line numbers
grep -rl 'pat' dir/ List files containing a match (no content)
grep -v 'noise' file Exclude lines matching a pattern
grep -C3 'pat' file Match with 3 lines of surrounding context
grep -c 'pat' file Count matching lines
grep -Fxq 'str' file Test if file contains an exact line (fixed, whole-line, quiet)
What is coming next: Chapter 8 covers sed and regex — the substitute command in depth, BRE vs ERE in sed, the -E flag, all substitution flags (g, i, N, p, w), replacement back-references, case-modifier sequences (\u \U \l \L), and address-targeted regex substitutions.