How to Learn Regular Expressions on Linux
Learn regex on Linux from the ground up: character classes, anchors, quantifiers, and hands-on practice with grep, sed, awk, and ripgrep.
Before you start
- ▸A terminal with bash or zsh
- ▸GNU coreutils installed (default on all major distros)
- ▸ripgrep installed for the rg sections (optional but recommended)
- ▸Read access to /var/log/syslog or /var/log/messages for practice examples
Regular expressions (regex) are patterns that describe text. Once you can read and write them, you unlock the full power of grep, sed, awk, ripgrep, and dozens of other tools. This guide teaches the core syntax and shows you how to apply it immediately on the command line — no external software required.
The Two Flavours You Will Actually Use
Linux tools split into two broad regex dialects. Knowing which one you are using prevents hours of confusion.
- BRE (Basic Regular Expressions) — the default in
grepandsed. Metacharacters like+,?, and{}must be backslash-escaped to be treated as operators. - ERE (Extended Regular Expressions) — activated with
grep -E(oregrep) andsed -E. Metacharacters work without escaping. Prefer this for anything non-trivial. - PCRE (Perl-Compatible Regular Expressions) — available via
grep -Pand natively inripgrep. Adds lookaheads, lookabehinds, and\d/\w/\sshortcuts.
The examples below use ERE (grep -E) unless stated otherwise.
Character Classes
A character class matches any single character from a set.
Literal sets
[aeiou] matches one vowel. [^aeiou] matches one character that is not a vowel — the caret inside brackets negates the class.
echo "hello world" | grep -Eo '[aeiou]+'
Output will vary, but you will see each run of vowels printed on its own line (e, o, o).
Ranges
[a-z] matches any lowercase letter; [0-9] matches any digit; [A-Za-z0-9] matches any alphanumeric character. Ranges depend on locale — set LC_ALL=C if results look wrong.
LC_ALL=C grep -Eo '[A-Z][a-z]+' /etc/os-release
POSIX named classes
More portable than ranges inside scripts intended to run on multiple distros:
[:alpha:]— letters[:digit:]— digits (equivalent to[0-9])[:alnum:]— letters and digits[:space:]— whitespace including tabs[:upper:]/[:lower:]— case classes
echo "root:x:0:0" | grep -Eo '[[:digit:]]+'
PCRE shorthand classes
When using grep -P or ripgrep: \d = digit, \w = word character, \s = whitespace. Their uppercase inverses (\D, \W, \S) negate the class.
echo "order #4821 total: $99" | grep -Po '\d+'
Anchors
Anchors do not consume characters — they assert a position in the string.
^— start of line$— end of line\b— word boundary (PCRE /grep -Porgrep -Ewith GNU extensions)\B— not a word boundary
# Lines that start with a comment character
grep -E '^#' /etc/ssh/sshd_config
# Lines that end with a semicolon
grep -E ';$' /etc/fstab
# Whole-word match for "root" — will not match "chroot"
grep -E '\broot\b' /etc/passwd
Quantifiers
Quantifiers control how many times the preceding element must match.
| Quantifier | Meaning | Example |
|---|---|---|
? | 0 or 1 | colou?r matches color and colour |
* | 0 or more | go*gle matches ggle, gogle, google |
+ | 1 or more | [0-9]+ matches one or more digits |
{n} | exactly n | [0-9]{4} matches a four-digit year |
{n,} | n or more | [a-z]{3,} matches words of 3+ letters |
{n,m} | between n and m | [0-9]{2,4} matches 2 to 4 digits |
# Match IPv4 octets (simplified)
echo "192.168.1.100" | grep -Eo '[0-9]{1,3}'
Greedy vs. lazy
By default quantifiers are greedy — they consume as much text as possible. In PCRE, append ? to make them lazy: +?, *?, {n,m}?.
# Greedy: matches everything between first < and last >
echo 'Home' | grep -Po '<.+>'
# Lazy: matches each tag individually
echo 'Home' | grep -Po '<.+?>'
Groups and Alternation
Parentheses create a capturing group that can be quantified or back-referenced. The pipe | expresses alternation.
# Match lines containing "failed" or "error" (case-insensitive)
grep -Ei '(failed|error)' /var/log/syslog | tail -20
# Extract month names from a log file
grep -Eo '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)' /var/log/auth.log | sort | uniq -c
Applying Regex with sed
sed is the go-to tool for in-place substitution. Use -E for extended regex and -i to edit files in place (always make a backup or test first without -i).
# Strip trailing whitespace from every line
sed -E 's/[[:space:]]+$//' file.txt
# Normalise multiple spaces to a single space
sed -E 's/[[:space:]]+/ /g' file.txt
# Capture groups in sed: swap first two colon-separated fields
echo "alice:1001:users" | sed -E 's/^([^:]+):([^:]+)/\2:\1/'
Back-references inside the replacement string use \1, \2, etc.
Applying Regex with awk
awk matches patterns against each record (line by default). The match() function returns the position of a match and sets RSTART and RLENGTH.
# Print lines where the third field is a four-digit number
awk '$3 ~ /^[0-9]{4}$/' data.txt
# Extract email addresses from a file
awk 'match($0, /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, arr) { print arr[0] }' contacts.txt
Note: the three-argument form of match() requires gawk (GNU awk), which is the default on most Linux distros.
Faster Search with ripgrep
ripgrep (rg) uses Rust's regex engine — it is typically 5–10× faster than GNU grep on large codebases and respects .gitignore by default.
Install ripgrep
# Debian / Ubuntu
sudo apt install ripgrep
# Fedora / RHEL 9+
sudo dnf install ripgrep
# Arch
sudo pacman -S ripgrep
# Find all TODO comments in a project, case-insensitive
rg -i 'todo|fixme' ~/projects/myapp
# Show only the matching portion (-o), with line numbers (-n)
rg -on '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' /var/log/nginx/access.log
A Practice Workflow
The fastest way to internalise regex is tight feedback loops. Use these techniques daily:
- Test patterns against
/var/log/syslog,/etc/passwd, or/proc/cpuinfo— real files with real variation. - Use
grep -o(orrg -o) to print only the matched portion so you can verify exactly what was captured. - Build patterns incrementally: match something broad first, then tighten it with anchors and quantifiers.
- Use
echowith a pipe to test a pattern before running it against a live file.
# Interactive: quickly test a pattern against sample input
echo "2024-07-15 ERROR disk full" | grep -Eo '[0-9]{4}-[0-9]{2}-[0-9]{2}'
Verification
After working through the sections above, confirm your understanding with this self-test. Each command should produce a clean, non-empty result:
# 1. Extract all usernames (field 1) from /etc/passwd that start with a letter
cut -d: -f1 /etc/passwd | grep -E '^[a-zA-Z][a-zA-Z0-9_-]*$'
# 2. Find log lines with a timestamp in HH:MM:SS format
grep -Eo '[0-2][0-9]:[0-5][0-9]:[0-5][0-9]' /var/log/syslog | head -5
# 3. Count distinct IP addresses in an nginx log
rg -o '[0-9]{1,3}(\.[0-9]{1,3}){3}' /var/log/nginx/access.log | sort -u | wc -l
Troubleshooting
- Pattern matches nothing: check whether you need
-E(ERE) or-P(PCRE). A stray backslash or missing-Eflag is the most common culprit. - Locale-dependent results: ranges like
[A-Z]can behave unexpectedly in UTF-8 locales. Prefix the command withLC_ALL=Cor use POSIX classes like[:upper:]. - sed in-place on macOS vs Linux: if you share scripts between systems, note that macOS
sedrequires-i ''rather than bare-i. Use GNU sed (gsed) on macOS for consistent behaviour. - Greedy quantifier consuming too much: switch to PCRE (
grep -Porrg) and use lazy quantifiers (+?,*?). - Special characters in the pattern: shell metacharacters (
*,?,[,$) must be inside single quotes or escaped, or the shell will expand them before grep sees them.
Frequently asked questions
- What is the difference between grep, grep -E, and grep -P?
- Plain grep uses Basic Regular Expressions where +, ?, and {} must be backslash-escaped. grep -E enables Extended Regular Expressions so those metacharacters work unescaped. grep -P activates Perl-Compatible Regular Expressions, adding \d, \w, lookaheads, and lazy quantifiers.
- Why does my regex match part of a word I did not intend?
- You probably need word-boundary anchors. Add \b around the pattern (e.g., \broot\b) with grep -E or grep -P to prevent matching substrings inside longer words like 'chroot'.
- How do I match a literal dot, asterisk, or other metacharacter?
- Escape it with a backslash: \. matches a literal period, \* matches a literal asterisk. Inside a character class, most metacharacters lose their special meaning so [.] also works for a literal dot.
- Is ripgrep a drop-in replacement for grep?
- For most interactive searching, yes. However rg skips binary files and respects .gitignore by default, it does not support BRE, and some GNU grep options differ. Use grep for scripting and POSIX compatibility; use rg for interactive speed.
- Why do ranges like [A-Z] sometimes match unexpected characters?
- In UTF-8 locales the collation order can cause [A-Z] to include characters beyond ASCII uppercase letters. Prefix your command with LC_ALL=C to force byte-order collation, or use the POSIX class [[:upper:]] which is locale-aware and portable.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.