What regex is and why it matters
A regular expression (regex) is a compact language for describing the *shape* of text instead of one fixed word. Rather than searching for the literal string admin, you can describe a pattern like "three digits, a dot, three digits, a dot..." to find every IP address in a file. In cybersecurity, regex is the workhorse of log analysis and threat hunting. SIEM correlation rules, IDS/IPS signatures (Snort and Suricata), firewall log parsers, and fast grep hunts all run on it. Learning to read and write patterns lets you pull every source IP out of an auth log, flag suspicious URLs, or validate input to block injection in seconds instead of scrolling.
This sheet uses extended regex (ERE), the flavor you get with grep -E. Most other tools (Python re, JavaScript, Suricata) share the same core syntax, so the tokens below transfer almost everywhere.
Core tokens at a glance
These are the metacharacters that give regex its power. A character with special meaning must be escaped with a backslash to match it literally.
| Token | What it matches | Example |
|---|---|---|
| . | Any single character except a newline | a.c matches "abc", "a7c", "a-c" |
| ^ | Start of the line (anchor, matches a position not a character) | ^Error matches lines starting with Error |
| $ | End of the line (anchor) | failed$ matches lines ending in failed |
| * | Zero or more of the preceding item | ab* matches "a", "ab", "abbb" |
| + | One or more of the preceding item | ab+ matches "ab", "abbb" (not "a") |
| ? | Zero or one of the preceding item (makes it optional) | colou?r matches "color" and "colour" |
| [ ] | Character class: any one character listed inside | [aeiou] matches any single vowel |
| [^ ] | Negated class: any one character NOT listed | [^0-9] matches any non-digit |
| ( ) | Group: bundle tokens so a quantifier or | applies to all of them | (ab)+ matches "ab", "abab" |
| | | Alternation (OR) between whole expressions | cat|dog matches "cat" or "dog" |
| {n} | Exactly n repetitions of the preceding item | [0-9]{3} matches exactly 3 digits, e.g. "404" |
| \ | Escape: treat the next metacharacter as a literal | \. matches a literal dot, not "any character" |
Character classes
A character class matches exactly one character out of a set. List the allowed characters in square brackets, use a hyphen for a range ([a-z]), and put a ^ first to negate the set ([^a-z]). The \d \w \s shorthands come from Perl/PCRE. They are convenient, but **\d is not portable to grep -E** (see the grep section), so the POSIX classes like [[:digit:]] are the safe choice in shell scripts.
| Class | Matches one | Example |
|---|---|---|
| [a-z] | Lowercase letter | [a-z]+ matches "hello" |
| [A-Z] | Uppercase letter | [A-Z]{2} matches "US" |
| [0-9] | Digit | [0-9]+ matches "48122" |
| [a-zA-Z0-9] | Letter or digit (combine ranges freely) | [a-zA-Z0-9]+ matches "web01" |
| \d | Digit, same as [0-9] (PCRE / grep -P only) | \d{4} matches "2026" |
| \w | Word character: letter, digit, or underscore | \w+ matches "user_01" |
| \s | Whitespace: space, tab, or newline | \s+ matches the gap between two words |
| \D \W \S | Negations: non-digit, non-word, non-whitespace | \D+ matches "port " |
| [[:digit:]] | POSIX class, portable in grep -E (also [[:alpha:]], [[:alnum:]], [[:space:]]) | [[:digit:]]{3} matches "200" |
Quantifiers: how many times
A quantifier attaches to the item just before it (a character, a class, or a group) and controls how many times it may repeat.
| Quantifier | Meaning | Example |
|---|---|---|
| * | Zero or more | lo*g matches "lg", "log", "looog" |
| + | One or more | lo+g matches "log", "looog" (not "lg") |
| ? | Zero or one (optional) | https? matches "http" and "https" |
| {n} | Exactly n times | [0-9]{4} matches a 4-digit year |
| {n,} | n or more times | [0-9]{2,} matches 2 or more digits |
| {n,m} | Between n and m times, inclusive | [0-9]{1,3} matches 1 to 3 digits (one IP octet) |
Quantifiers are greedy by default
By default *, +, and {n,} grab as much text as possible. Against the text <a><b>, the pattern <.*> matches the whole thing, not just <a>. Perl and grep -P let you add a ? to make a quantifier lazy (<.*?>), but POSIX grep -E has no lazy quantifiers. The portable fix is to narrow the class so it cannot cross the boundary: <[^>]*> matches a single tag.
Anchors, groups, and alternation
Anchors match a position, not a character, so they pin a pattern to where it appears. ^ ties the match to the start of the line and $ to the end. \b matches a word boundary (the edge between a word character and a non-word character), which stops \bcat\b from matching "category".
Groups with ( ) let a quantifier or an alternation apply to several tokens at once, and alternation with | means "this OR that". Combine them to write one pattern that catches several cases, for example ^(ERROR|WARN|FATAL) to grab every high-severity log line.
| Token | Meaning | Example |
|---|---|---|
| ^ | Start of line | ^Failed matches a line beginning with Failed |
| $ | End of line | \.log$ matches a filename ending in .log |
| \b | Word boundary (GNU grep and PCRE) | \broot\b matches "root" but not "chroot" |
| (...) | Group tokens together | (ab){3} matches "ababab" |
| a|b | Match a or b | GET|POST matches either HTTP method |
| (x|y)z | Group + alternation share a suffix | (ssh|ftp)d matches "sshd" or "ftpd" |
Two patterns worth memorizing
These are practical, log-grepping patterns. They are deliberately loose so they are short and easy to remember. They find well-formed text but do not fully validate it, for example the IP pattern will happily match 999.999.999.999. For hunting through logs that is fine. For strict input validation you would tighten each octet to (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).
| Pattern | Matches | Example match |
|---|---|---|
| ([0-9]{1,3}\.){3}[0-9]{1,3} | A dotted IPv4 address (loose) | 203.0.113.9 |
| [0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5} | A MAC address | 00:1a:2b:3c:4d:5e |
| [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,} | An email address (practical, not RFC-complete) | will@example.com |
| ^[0-9]{4}-[0-9]{2}-[0-9]{2} | An ISO date at the start of a line | 2026-08-20 |
| https?://[^ ]+ | An http or https URL | https://example.com/login |
Escape the dot in an IP pattern
Inside the pattern, an unescaped . means "any character", so the pattern 192.168.0.1 would also match the text "192x168y0z1". Write the literal dots as \. (as in the IP row above) so the pattern only matches real dotted addresses.
Using regex with grep -E
Plain grep uses basic regex (BRE), where +, ?, {, |, and () are literal characters and need a backslash to act special. That is confusing, so for pattern work always reach for grep -E (extended regex), where those operators just work. For the Perl \d shorthand and lookaround, use grep -P instead.
The flags you will combine most often when searching logs.
| Flag | What it does | Example |
|---|---|---|
| grep PATTERN file | Global Regular Expression Print (named after the ed editor command g/re/p) - search a file line by line and print every line that matches the pattern. | grep 'Failed password' auth.log |
| -E | Extended regex - use extended regex so +, ?, {}, |, () work without backslashes | grep -E 'error|fail' app.log |
| -P | Perl regex - use PCRE, which enables \d, \b, and lookaround | grep -P '\d{3}' access.log |
| -o | only-matching - print only the matched text, not the whole line | grep -oE '[0-9]+' access.log |
| -i | ignore case - match regardless of upper or lower case | grep -iE 'password' auth.log |
| -c | count - print a count of matching lines instead of the lines themselves | grep -cE '^Failed' auth.log |
| -v | invert - show lines that do NOT match | grep -vE '^#' sshd_config |
| -n | line number - prefix each match with its line number in the file | grep -nE 'root' auth.log |
| -r | recursive - search every file under a directory tree, not just one file | grep -rE 'BEGIN PRIVATE KEY' /home |
| -l | list files - print only the NAMES of files that contain a match, not the matching lines | grep -rlE 'password' /etc |
| -q | quiet - print nothing and exit 0 as soon as a match is found (handy in shell scripts) | grep -qE 'root' auth.log && echo present |
\d does not work in grep -E
This trips up almost everyone. In grep and grep -E, the pattern \d matches a literal letter d, not a digit, so grep -E '\d' gives wrong results silently. Use [0-9] or [[:digit:]] in ERE, or switch to grep -P '\d'. As a quirk, GNU grep does accept \w, \s, and \b as extensions in -E, but \d is the one that fails, so prefer [0-9] to stay safe and portable.
Test a pattern before you trust it
Feed a known string into grep to confirm a pattern behaves before running it across gigabytes of logs (echo just prints its text so grep has something to read). echo "203.0.113.9" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' should print the address back. If it prints nothing, your pattern is wrong, not the data. An online tester like regex101 is great for building patterns interactively.
Try it yourself
app.logDownload app.log: https://restedteacher.com/practice-files/cybersecurity/app.log
A 28-line sample application log mixing ERROR, WARN, FATAL, INFO, DEBUG, and TRACE lines, built to test the ^(ERROR|WARN|FATAL) anchor pattern and the escaped-dot IP pattern.
contacts.txtDownload contacts.txt: https://restedteacher.com/practice-files/cybersecurity/contacts.txt
A short fictional contact directory for practicing grep -oE email extraction.
Get to the files first
Your browser saves both files to your Downloads folder. Move them into a labs folder and go there:
mkdir -p ~/labs && mv ~/Downloads/app.log ~/Downloads/contacts.txt ~/labs/ && cd ~/labs
Then run ls and confirm you can see both app.log and contacts.txt. On WSL, your Windows Downloads folder is /mnt/c/Users/<your-name>/Downloads instead of ~/Downloads.
1. Write one pattern that matches only the high-severity lines of app.log: those beginning with ERROR, WARN, or FATAL. Hint: combine the ^ anchor with alternation and a group. Run it with grep -E '<pattern>' app.log and count the matches with -c. Does the line that only mentions the word ERROR in the middle of an INFO line count? Does the line that starts with lowercase warn count?
2. Use grep -oE with the email pattern from this sheet against contacts.txt to extract every address, one per line. How many does it find — and why doesn't it pick up the line that spells the address out in words, or the line with root@localhost?
3. Near the end of app.log are two lines built to test the escaped-dot IP pattern: one address written with real dots, one with the dots swapped for x. Run the loose IP pattern from this sheet with grep -oE against the file and see which of the two gets extracted. Then explain in one sentence why an unescaped . in the pattern would have matched both.