$linuxjunkies
>

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.

BeginnerUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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 grep and sed. Metacharacters like +, ?, and {} must be backslash-escaped to be treated as operators.
  • ERE (Extended Regular Expressions) — activated with grep -E (or egrep) and sed -E. Metacharacters work without escaping. Prefer this for anything non-trivial.
  • PCRE (Perl-Compatible Regular Expressions) — available via grep -P and natively in ripgrep. Adds lookaheads, lookabehinds, and \d/\w/\s shortcuts.

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 -P or grep -E with 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.

QuantifierMeaningExample
?0 or 1colou?r matches color and colour
*0 or morego*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:

  1. Test patterns against /var/log/syslog, /etc/passwd, or /proc/cpuinfo — real files with real variation.
  2. Use grep -o (or rg -o) to print only the matched portion so you can verify exactly what was captured.
  3. Build patterns incrementally: match something broad first, then tighten it with anchors and quantifiers.
  4. Use echo with 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 -E flag is the most common culprit.
  • Locale-dependent results: ranges like [A-Z] can behave unexpectedly in UTF-8 locales. Prefix the command with LC_ALL=C or use POSIX classes like [:upper:].
  • sed in-place on macOS vs Linux: if you share scripts between systems, note that macOS sed requires -i '' rather than bare -i. Use GNU sed (gsed) on macOS for consistent behaviour.
  • Greedy quantifier consuming too much: switch to PCRE (grep -P or rg) 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.
tested on:Ubuntu 24.04Fedora 40Arch 2024.06.01Debian 12

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