Skip to content

grep and Regex

grep prints lines that match a pattern, and regular expressions describe the pattern. Filtering logs, checking configuration and extracting fields during an incident all start here, and interviews test both the flags and the regex syntax.

Track: Core · Interview weight: High


Must-Know Facts

Fact Value Verify with
Common flags -i ignore case, -v invert, -n line numbers, -c count, -w whole word, -x whole line grep -in error app.log
Output only the match -o grep -o '[0-9]*' f
Context -A N after, -B N before, -C N both grep -C2 ERROR log
Recursive -r (-R follows symlinks); --include, --exclude, --exclude-dir grep -r --include='*.conf' x /etc
File names only -l matching, -L not matching grep -rl TODO .
Quiet test -q; status 0 match, 1 no match, 2 error grep -q x f; echo $?
Regex flavors Basic (default), -E extended, -P Perl, -F fixed string grep -E 'a+'
BRE vs ERE In BRE, +, ?, the bar, parentheses and braces need a backslash to be special grep 'a\+'
Anchors ^ line start, $ line end, \b word boundary grep '^#'
Classes [0-9], [^a-z], [[:space:]], [[:digit:]]; \s, \w in GNU grep grep '[[:digit:]]'
Repetition * 0+, + 1+, ? 0 or 1, {n,m} (ERE) grep -E 'a{2,}'
Dot Any character; \. is a literal dot grep -c '\.'
Binary files Reports "binary file matches"; -a treats them as text grep -a

Basic Matching

All examples run against the sample access log from Viewing and Comparing.

grep 'POST' access.log
grep -c ' 500 ' access.log
grep -n 'admin' access.log
grep -i 'mozilla' access.log | wc -l
grep -v 'kube-probe' access.log | wc -l
grep -w 'on' app-v2.conf
grep -x 'tls = enabled' app-v2.conf

Output:

192.0.2.10 - - [16/Sep/2026:14:10:07 +0000] "POST /login HTTP/1.1" 401 118 "-" "Mozilla/5.0" 0.041
192.0.2.10 - - [16/Sep/2026:14:11:21 +0000] "POST /login HTTP/1.1" 200 4634 "-" "Mozilla/5.0" 0.808
203.0.113.99 - - [16/Sep/2026:14:15:52 +0000] "POST /login HTTP/1.1" 401 118 "-" "Mozilla/5.0" 0.037
4
13:203.0.113.5 - - [16/Sep/2026:14:14:24 +0000] "GET /admin HTTP/1.1" 403 790 "-" "Mozilla/5.0" 0.092
6
15
feature_x = on
tls = enabled

-w matched on as a whole word and skipped # app configuration, where on is only part of a word.


Regular Expressions

Element Meaning Example
. Any single character h.t matches hat, hot
* Previous item 0 or more times ab*c
+ (ERE) 1 or more [0-9]+
? (ERE) 0 or 1 https?
{n,m} (ERE) Between n and m times [0-9]{1,3}
[abc], [^abc] One of, none of [^#]
^, $ Line start, line end ^$ is an empty line
Vertical bar (ERE), backslash and bar (BRE) Alternation: either side matches GET or POST in one pattern
( ) (ERE) Group (ab)+
\1 Back-reference to group 1 (.)\1 finds doubled characters
\b, \<, \> Word boundaries (GNU) \bport\b

Extended and basic syntax give the same result when the special characters are escaped for BRE:

grep -E '" (4|5)[0-9]{2} ' access.log | wc -l
grep '" \(4\|5\)[0-9]\{2\} ' access.log | wc -l
grep -oE '"(GET|POST) [^ ]+' access.log | sort | uniq -c | sort -rn | head -3
grep -Ev '^\s*(#|$)' app-v1.conf

Output:

8
8
      3 "POST /login
      3 "GET /static/app.js
      3 "GET /
listen_port = 8080
db_host = db01.internal
db_pool = 10
log_level = info
feature_x = off

The last command removes comments and blank lines, the fastest way to read a long default configuration file.

Special characters behave differently in each flavor:

grep -E 'a{2,}' <<< $'a\naa\naaa'
echo 'price: $5' | grep '\$5'
echo 'a+b' | grep 'a+b'
echo 'a+b' | grep -E 'a+b'
echo 'a+b' | grep -E 'a\+b'
grep -E '^(.)(.).?\2\1$' <<< $'abba\nabcba\nabcd'

Output:

aa
aaa
price: $5
a+b
a+b
abba
abcba

In basic syntax, + is a literal plus; in extended syntax it means "one or more", so -E 'a+b' found no ab in a+b and printed nothing. The back-reference example finds four- and five-letter palindromes.

Quote every pattern

Unquoted, grep [0-9]* file is expanded by the shell if a matching file exists, and $ or \ are processed before grep sees them. Single quotes pass the pattern unchanged.


Extracting Values

grep -o '^[0-9.]*' access.log | sort -u
grep -oP '(?<=" )\d{3}(?= )' access.log | sort | uniq -c
grep -oP '\d+\.\d+$' access.log | sort -n | tail -1

Output:

192.0.2.10
192.0.2.44
198.51.100.23
198.51.100.7
203.0.113.5
203.0.113.99
      7 200
      2 301
      2 401
      1 403
      1 404
      4 500
1.855

-P enables Perl syntax: \d, and look-behind (?<=...) and look-ahead (?=...), which match context without including it in -o output. -P exists in GNU grep, not in BSD or BusyBox grep.


Context, Recursion and Files

grep -B1 -A1 -n '/admin' access.log | cut -c1-40
grep -r 'listen_port' conf.d
grep -rl 'tls' conf.d
grep -rL 'tls' conf.d
grep -r --include='*.conf' 'db_pool' conf.d

Output:

12-203.0.113.5 - - [16/Sep/2026:14:13:17
13:203.0.113.5 - - [16/Sep/2026:14:14:24
14-192.0.2.44 - - [16/Sep/2026:14:14:31 
conf.d/b.conf:listen_port = 8443
conf.d/a.conf:listen_port = 8080
conf.d/b.conf
conf.d/notes.txt
conf.d/a.conf
conf.d/b.conf:db_pool = 20
conf.d/a.conf:db_pool = 10

With -n, a colon marks the matching line and a dash marks context lines.


Exit Status, Binaries and Self-Matches

grep -q 'tls' app-v2.conf && echo "tls configured"
grep 'nomatch' app-v2.conf; echo "rc=$?"
grep 'x' /nope; echo "rc=$?"
grep GLIBC_2.34 /usr/bin/ls
grep -a -o 'GLIBC_2\.[0-9]*' /usr/bin/ls | sort -uV | tail -2

Output:

tls configured
rc=1
grep: /nope: No such file or directory
rc=2
grep: /usr/bin/ls: binary file matches
GLIBC_2.34
GLIBC_2.38

The last command lists the newest glibc symbol versions a binary needs, which explains GLIBC_2.38 not found when it runs on an older distribution.

grep also matches its own command line in ps output:

sleep 100 &
ps aux | grep sleep
ps aux | grep '[s]leep'
pgrep -a sleep

Output:

laborant    3128  0.0  0.0   6116  1004 ?        S    14:23   0:00 sleep 100
laborant    3130  0.0  0.0   7080  2060 ?        S    14:23   0:00 grep sleep
laborant    3128  0.0  0.0   6116  1004 ?        S    14:23   0:00 sleep 100
3128 sleep 100

[s]leep still matches sleep, but the grep command line contains [s]leep, which the pattern does not match. pgrep avoids the problem entirely.

grep -F for fixed strings

Searching for text with dots, brackets or slashes (1.1" 5, [error], IP addresses) is safer and faster with -F, which disables regex parsing.


Common Errors

grep: /usr/bin/ls: binary file matches

Cause: the file contains NUL bytes, so grep suppresses the matching line.

Fix: grep -a to treat it as text, or strings <file> | grep <pattern>.

grep: Invalid regular expression

Cause: an unbalanced bracket or brace, such as grep '['.

Fix: escape the character ('\[') or use grep -F for literal text.


Interview Checkpoints

L1: What is the difference between grep, grep -E and grep -F?

Say first: basic regex needs backslashes for + ? | ( ) { }; -E makes them special without escapes; -F treats the pattern as a fixed string.

Proof: grep -E 'a+b' does not match a+b; grep 'a+b' does.

Follow-up: When is -P needed?

L1: What does grep return when nothing matches, and why does it matter?

Say first: exit status 1 for no match and 2 for an error, so scripts can distinguish them.

Proof: grep nomatch file; echo $? prints 1.

Follow-up: What does that do to a set -e script?

L2: Show the configuration file without comments and blank lines.

Say first: invert-match an extended pattern.

Proof: grep -Ev '^\s*(#|$)' /etc/ssh/sshd_config

Follow-up: How would you also drop lines that start with ;?

L2: Count requests per HTTP status code in an access log.

Say first: extract the status field and count.

Proof: grep -oP '(?<=" )\d{3}(?= )' access.log | sort | uniq -c

Follow-up: How does awk '{print $9}' compare?

L2: List every file under /etc that mentions a hostname, without printing the lines.

Say first: recursive, file names only.

Proof: sudo grep -rl 'db01.internal' /etc

Follow-up: How do you skip a directory? (--exclude-dir.)

L2: Show five lines before and after every OutOfMemoryError in a log.

Say first: context flags.

Proof: grep -n -C5 OutOfMemoryError app.log

Follow-up: How do you read compressed rotated logs the same way? (zgrep.)

L3: A monitoring script reports a process as running even after it was stopped.

Say first: check whether the check matches its own grep.

Proof: ps aux | grep myapp | wc -l returns 1 because of the grep line; pgrep -x myapp or grep '[m]yapp' fixes it.

Follow-up: Why is pgrep -f also risky? (It matches full command lines, including the script's.)

L3: A binary fails with version GLIBC_2.38 not found on an older server.

Say first: compare the glibc versions the binary needs with the ones installed.

Proof: grep -a -o 'GLIBC_2\.[0-9]*' ./app | sort -uV | tail -1 against ldd --version.

Follow-up: What are the fixes? (Build on the oldest target, static linking, or a container.)


Captured on Rocky Linux 10.2 and Ubuntu 24.04.4 LTS (iximiuz Labs microVMs, kernel 6.1.167), 2026-09.