Skip to content

grep: Patterns and Power

Chapter 2's patterns were plain words. A pattern can also describe: "a digit, then another digit," "ERROR or WARN," "an address beginning 192.168." The describing language is regular expressions — regex — and it is a subject of its own: the sibling guide Regular Expressions for Mere Mortals teaches it properly. This chapter borrows only the pieces grep work needs daily, and spends its length on the grep-specific machinery around them: which dialect you're speaking, several patterns at once, whole trees, and exit codes.

Classes and repetition, briefly

Square brackets name a set of acceptable characters for one position: [0-9] is "any one digit." One — that word does more damage than any other in pattern work:

$ grep 'usage at [0-9]%' app.log

Nothing. The log's figures are two digits (81%), and [0-9] covers exactly one character position, so the pattern demands a digit immediately followed by %. Say it twice and it matches:

$ grep 'usage at [0-9][0-9]%' app.log
2026-10-17 13:14:59 WARN disk usage at 81% on /data
2026-10-18 13:37:48 WARN disk usage at 84% on /data
2026-10-19 13:45:21 WARN disk usage at 79% on /data

A dot, remember, means any character — which is why an address pattern wants its dots escaped with backslashes, each \. meaning one literal dot:

$ grep -c 'ip=192\.168\.1\.' app.log
14

(The count is 14 — nine lines from 192.168.1.10 and five from 192.168.1.25. Unescaped, the same command also answers 14 on this file, because the addresses really do contain dots. Chapter 2's nonsense address, 192x168y1z10, is what the escape protects you from.)

Two dialects: basic and extended

grep's default dialect is the basic regular expression (BRE), a historical dialect in which the powerful characters +, ?, |, (, ) are literal unless escaped with a backslash. The extended dialect (-E, once a separate program called egrep) makes them operators. The two-digit problem above, in extended form, uses + — "one or more of the thing before":

$ grep -E 'usage at [0-9]+%' app.log
2026-10-17 13:14:59 WARN disk usage at 81% on /data
2026-10-18 13:37:48 WARN disk usage at 84% on /data
2026-10-19 13:45:21 WARN disk usage at 79% on /data

The same pattern in basic grep needs the backslash spelling, \+:

$ grep 'usage at [0-9]\+%' app.log
2026-10-17 13:14:59 WARN disk usage at 81% on /data
2026-10-18 13:37:48 WARN disk usage at 84% on /data
2026-10-19 13:45:21 WARN disk usage at 79% on /data

Same answers, two spellings. Mortals generally pick -E and stay there. With -E, | means or, and parentheses group:

$ grep -Ec 'ERROR|WARN' app.log
24
$ grep -E 'user=(paul|katherine)' app.log
2026-10-17 07:15:22 INFO auth login ok user=paul ip=192.168.1.10
2026-10-17 10:22:08 INFO auth login ok user=katherine ip=192.168.1.25

(and eight more lines — every line naming either user, logins and logouts alike). That one pattern replaces two greps and a merge, and it's the shape of most real-world searching: any of these words, in this position.

Perl patterns, named honestly

GNU grep has a third dialect, -P, which speaks the Perl dialect of regex: \d for a digit, {4} for "exactly four," and more. Two of its tricks earn a showing. -o prints only the matched part of each line instead of the whole line, and \K means "the match starts keeping from here," which strips a prefix in one motion:

$ grep -oP 'ip=\K[0-9.]+' app.log | head -4
192.168.1.10
192.168.1.10
198.51.100.23
198.51.100.23
$ grep -oP '\d{4}-\d{2}-\d{2}' app.log | head -3
2026-10-17
2026-10-17
2026-10-17

Now the honesty: -P is a GNU feature. macOS and the BSDs don't have it — their grep answers "invalid option" — and even among GNU greps it can be missing from minimal builds. Everything in this guide except this section runs on any grep; treat -P as a local convenience, not a portable habit, and reach for it when you're working interactively on a GNU system (chapter 8 extracts the same addresses with awk, the portable way).

Many patterns at once

-e may be repeated to give grep several patterns — a line matching any of them prints:

$ grep -e timeout -e disk-full app.log
2026-10-17 09:58:47 ERROR app job import-orders failed reason=timeout
2026-10-17 20:47:33 ERROR app job export-report failed reason=disk-full
2026-10-18 11:42:29 ERROR app job import-orders failed reason=timeout

And -f reads the patterns from a file, one per line — the form that scales to a watchlist you maintain:

$ cat patterns.txt
locked
heartbeat
$ grep -f patterns.txt app.log
2026-10-17 22:03:19 INFO app heartbeat ok ip=10.0.0.5
2026-10-18 08:14:55 ERROR auth account locked user=admin ip=198.51.100.23
2026-10-18 22:03:19 INFO app heartbeat ok ip=10.0.0.5
2026-10-19 22:03:19 INFO app heartbeat ok ip=10.0.0.5

(patterns.txt is yours to make — the lab doesn't ship it, because making it is the lesson.)

Searching a tree

-r (recursive) searches a directory and everything under it, prefixing each match with its path. With a small specimen tree — site/a.log is a copy of the lab log, and site/readme.txt is a note saying all's quiet:

$ grep -r 'account locked' site
site/a.log:2026-10-18 08:14:55 ERROR auth account locked user=admin ip=198.51.100.23
$ grep -rl 'ERROR' site
site/a.log

The readme doesn't appear: it says "errors," lowercase, and grep is case-sensitive unless -i says otherwise. --include narrows the search to files matching a name pattern:

$ grep -rl --include='*.txt' 'errors' site
site/readme.txt

Recursive search over a big tree is the one grep in this guide that can take a while or say a lot; -l and -c are how you keep it civilised — survey first, read second.

Exit codes: grep's second answer

Every grep gives two answers: the lines it prints, and an exit code — a number the shell keeps in $? — reporting how it went. grep's are:

Code Meaning
0 Lines were selected (at least one match)
1 No lines selected
2 An error occurred (a file missing, unreadable, …)

All three, captured:

$ grep ERROR app.log > /dev/null; echo $?
0
$ grep CRITICAL app.log; echo "grep exit was $?"
grep exit was 1
$ grep ERROR missing.log; echo "grep exit was $?"
grep: missing.log: No such file or directory
grep exit was 2

The distinctions matter because 1 is not an error — "nobody knocked today" is a finding, not a failure — while 2 means the question itself went wrong. Scripts lean on this constantly, usually with -q (quiet: print nothing, the exit code is the whole answer):

$ grep -q ERROR app.log; echo $?
0
$ grep -q CRITICAL app.log; echo $?
1

if grep -q 'failed' app.log; then …; fi is the shape. Chapter 10 returns to these codes, because "grep printed nothing and I don't know why" is the most common text trouble there is — and the answer is almost always sitting in $?.

Next: sed, which doesn't just find lines — it changes them.