Regular Expressions Explained: How Regex Patterns Work

Sooner or later every command line session runs into a matching problem that plain text search cannot solve: find lines that start with a number, pull every email address out of a log, or replace dates in one format with another. Regular expressions (regex) are the pattern language that solves these problems, and the same core syntax works in grep, sed, awk, text editors, and most programming languages.
This guide explains the building blocks of regular expressions, anchors, character classes, quantifiers, grouping, and alternation, and shows how the same pattern carries from grep to sed and awk.
What a Regular Expression Is
A regular expression is a pattern that describes a set of strings. Instead of matching one literal word, a pattern like ^error [0-9]+ matches any line that starts with “error” followed by a space and one or more digits.
The examples in this guide use GNU grep -E (extended regular expressions), so every pattern works as written. You can test any of them by piping text into grep
:
echo "error 404" | grep -E '^error [0-9]+'error 404The line prints because it matches the pattern. When there is no match, grep prints nothing and returns a non-zero exit status. Always single-quote the pattern so the shell does not interpret characters like $ and * before grep sees them.
Literal Characters and Metacharacters
Most characters in a regex match themselves: the pattern cat matches the string “cat” anywhere in a line, including inside “concatenate”. A handful of characters have special meanings instead of matching literally:
. ^ $ * + ? ( ) [ ] { } | \These are the metacharacters, and the rest of this guide is about what they do. To match one of them literally, escape it with a backslash: \. matches a real dot, \$ a real dollar sign.
Anchors and Word Boundaries
Anchors and word boundaries do not match characters; they match positions in the line.
^- Matches the start of the line.$- Matches the end of the line.\b- Matches a word boundary, the position between a word character and a non-word character.
The \b boundary is a GNU grep extension rather than part of POSIX extended regular expression syntax.
The difference is easiest to see on real input. The following input contains three similar lines, but the pattern prints only the line that consists of exactly the word “root”:
printf 'root\nroot:x:0:0\nchroot\n' | grep -E '^root$'rootWithout anchors, the pattern root would also match “chroot” or a line where “root” appears in the middle. Word boundaries solve the substring problem without pinning the match to the whole line:
echo "the cat scattered" | grep -E -o '\bcat\b'catThe -o flag prints only the matched text. Notice that “scattered” did not produce a match, because “cat” inside it is not surrounded by word boundaries.
Character Classes: Matching Sets
Square brackets match one character from a set:
[abc]- One character: a, b, or c.[a-z]- One lowercase letter; ranges also work for[0-9]and[A-Z].[^abc]- Negation: one character that is NOT a, b, or c.
For example, to match “gray” and “grey” with one pattern:
printf 'gray\ngrey\ngroy\n' | grep -E 'gr[ae]y'gray
greyThe third line does not match because “o” is not in the set. Inside brackets, most metacharacters lose their special meaning; [.] matches a literal dot.
POSIX character classes are named shortcuts that work inside brackets: [[:digit:]] is equivalent to [0-9], [[:alpha:]] matches letters, [[:space:]] matches whitespace. Many tools also support the Perl-style shorthands \d wherever PCRE is available (grep -P), but the bracket forms are the portable choice for shell work.
The Dot and Quantifiers: Matching Repetition
The dot . matches any single character except a newline. Quantifiers apply to the preceding item and control how many times it may repeat:
*- Zero or more times.+- One or more times.?- Zero or one time (optional).{n}- Exactly n times.{n,m}- Between n and m times.
Combining the dot with a quantifier gives .*, which matches anything, including nothing. A more precise example matches an IPv4-looking address by requiring one to three digits in each group:
echo "server at 192.168.1.10 is up" | grep -E -o '[0-9]{1,3}(\.[0-9]{1,3}){3}'192.168.1.10Reading it piece by piece: [0-9]{1,3} matches the first number group, \. matches a literal dot, and the parentheses with {3} repeat the dot-plus-number sequence three times.
A common beginner mistake is reaching for * when + is meant. The pattern [0-9]* happily matches an empty string, so it succeeds on every line; [0-9]+ actually requires a digit.
Grouping and Alternation
Parentheses group parts of a pattern, and the pipe | provides alternation (OR):
printf 'error: disk full\nwarning: low memory\ninfo: started\n' | grep -E '^(error|warning):'error: disk full
warning: low memoryThe group limits the alternation to the two words before the colon; without parentheses, ^error|warning: would mean “starts with error, OR contains warning: anywhere”, which is rarely what you want.
Groups also capture what they match, and the captured text can be reused. In sed
, \1 refers to the first group, which makes reordering text possible:
echo "2026-01-15" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3.\2.\1/'15.01.2026The three groups capture the year, month, and day, and the replacement writes them back in reverse order.
Basic vs Extended Regular Expressions
POSIX defines two regex dialects, and the difference trips up almost everyone at some point. In basic regular expressions (BRE), which plain grep and sed use, the characters +, ?, |, {}, and () match literally, and you must escape them (\+, \( … \)) to get the special behavior. In extended regular expressions (ERE), they are special by default.
| Construct | ERE (grep -E, sed -E, awk) | BRE (plain grep, sed) |
|---|---|---|
| One or more | + | \+ |
| Optional | ? | \? |
| Repetition | {n,m} | \{n,m\} |
| Grouping | (...) | \(...\) |
| Alternation | a|b | a|b, GNU extension only |
That last row is the one to watch. Alternation is not part of POSIX BRE at all, so \| works in GNU grep and GNU sed but fails silently on the BSD sed that ships with macOS.
In practice, the simplest rule is: pass -E to grep and sed and write in the extended dialect, as every example in this guide does. awk uses extended syntax natively:
printf 'alice 92\nbob 47\ncarol 78\n' | awk '/^[ab]/ {print $1}'alice
bobThe awk
pattern selects lines starting with “a” or “b” and prints the first field. grep -P enables a third dialect, Perl-compatible regular expressions (PCRE), which adds features like \d and lookarounds; reach for it when the POSIX dialects run out.
One Pattern, Three Tools
The syntax is shared, but each tool wraps it differently, which is the part that usually causes confusion when moving a working pattern from one command to another. Create a small log file to follow along:
printf 'error: disk full\nwarning: low memory\ninfo: started\n' > app.logBecause grep is a filter, the pattern is the whole job and needs nothing around it:
grep -E '^(error|warning):' app.logsed prints every input line unless you suppress that with -n, so selecting lines takes an address followed by an explicit p command:
sed -E -n '/^(error|warning):/p' app.logawk reads a bare pattern with no action block as “print the matching line”:
awk '/^(error|warning):/' app.logerror: disk full
warning: low memoryAll three print the same two lines and skip the “info” line. Where the tools part company is what happens after the match. grep reports lines and stops there, sed can rewrite the matched text, and awk splits each matching line into fields you can work with:
awk -F': ' '/^(error|warning):/ {print $2}' app.logdisk full
low memorySetting the field separator to ': ' makes $2 the message text, so the pattern selects the lines and the action block pulls out the part you actually wanted. This is why it pays to learn regex once as its own subject: the pattern you write today for grep is the same pattern you will paste into sed, awk, a text editor, or a Python script tomorrow.
Quick Reference
| Pattern | Matches |
|---|---|
. | Any single character |
^ / $ | Start / end of line |
\b | Word boundary in GNU grep |
[abc] / [^abc] | One of the set / one not in the set |
[0-9], [[:digit:]] | One digit |
* / + / ? | Zero or more / one or more / optional |
{n,m} | Between n and m repetitions |
(foo|bar) | foo or bar |
\1 | Text captured by the first group |
\. | Literal dot (escaped metacharacter) |
Conclusion
Anchors, character classes, quantifiers, and groups combine into patterns that handle most day-to-day matching on Linux, and the same vocabulary carries over to editors and programming languages. To put the syntax to work in specific tools, see our guides to regular expressions in grep and find and replace with sed .
Tags
Linuxize Weekly Newsletter
A quick weekly roundup of new tutorials, news, and tips.
About the authors

Dejan Panovski
Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 800+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.
View author page