Text Processing · about 45 min

Searching with grep and basic regular expressions

Why this matters

"Did the job fail last night? Which config files set this option? Which users logged in from outside the office?" Most questions about a system start as "find the lines that...". grep is the tool that answers them, and it is fast enough to search gigabytes in seconds. Its pattern language, regular expressions, is used by sed, awk, bash's [[ =~ ]], your editor and nearly every programming language, so what you learn here pays off for years.

grep in one sentence

grep PATTERN [FILE...] prints every line that contains a match for PATTERN. With no file, it reads standard input, so it works as a filter in pipelines.

Open the shell for this lesson; app.log is waiting in /work.

$ grep ERROR app.log
2026-10-30 08:06:10 ERROR payment gateway timeout order=1042
2026-10-30 08:20:30 ERROR template not found: invoice.html user=ben
2026-10-30 08:31:45 ERROR database connection lost
$ grep user=ben app.log
2026-10-30 08:12:51 INFO login user=ben
2026-10-30 08:20:30 ERROR template not found: invoice.html user=ben
2026-10-30 08:21:00 INFO logout user=ben

grep reads the input one line at a time and tests each line independently. A match anywhere in the line selects the whole line. Always quote the pattern (single quotes are safest): many regex characters, like *, ?, [ and $, are also special to bash, and you want grep to see them, not bash.

The options you will use every day

OptionEffect
-iignore case: error matches ERROR and Error
-vinvert: print lines that do not match
-cprint the count of matching lines instead of the lines
-nprefix each line with its line number
-wmatch whole words only: -w cat does not match concat
-oprint only the matched part, one per line
-lprint only names of files that contain a match
-rsearch directories recursively
-qquiet: print nothing, just set the exit status
-Eextended regular expressions (below)
-Ffixed string: no regex at all, every character is literal
-A N, -B N, -C Nalso print N lines after / before / around each match
$ grep -c INFO app.log
12
$ grep -vc INFO app.log
7
$ grep -n WARN app.log
3:2026-10-30 08:03:17 WARN slow query took 2.4s user=ana
9:2026-10-30 08:13:02 WARN disk usage at 85%
14:2026-10-30 08:31:46 WARN reconnecting to database
17:2026-10-30 08:44:20 WARN password attempt failed user=dmitri
$ grep -o 'user=[a-z]*' app.log | head -n 3
user=ana
user=ana
user=ben

With several files, grep prefixes each line with the file name. -r walks a directory tree and -l reduces the output to one name per file:

$ grep -rl TODO src
src/main.sh
src/tests/run.sh
src/lib/util.sh

The order is the order in which grep meets the files in the directory, which depends on the filesystem. Pipe through sort when you need a stable order.

Exit status

grep's exit status is designed for scripts: 0 if any line matched, 1 if none did, 2 if there was an error (such as a missing file). grep -q combines this with silence:

grep -q '^debug=true' app.conf && echo "debug is on"

Regular expressions

A regular expression (regex) is a small pattern language describing sets of strings. Most characters match themselves: the regex ERROR matches the five letters E, R, R, O, R in a row. A few characters are special (metacharacters):

RegexMatches
.any single character
[abc]one character from the set: a, b or c
[a-z], [0-9]one character from a range
[^0-9]one character not in the set
^the start of the line (no character, a position)
$the end of the line
x*zero or more of x (x is the item just before)
\.a literal dot (backslash removes specialness)

Examples against app.log:

$ grep '^2026-10-30 08:0' app.log | wc -l
7
$ grep 'database$' app.log
2026-10-30 08:00:02 INFO connected to database
2026-10-30 08:31:46 WARN reconnecting to database
2026-10-30 08:31:49 INFO connected to database
$ grep 'order=[0-9][0-9]*' app.log
2026-10-30 08:06:10 ERROR payment gateway timeout order=1042
2026-10-30 08:06:13 INFO payment ok order=1042

[0-9][0-9]* reads "one digit, then zero or more digits", in other words "one or more digits".

Character classes with names are portable across languages and locales: [[:digit:]], [[:alpha:]], [[:space:]], [[:upper:]], [[:alnum:]], [[:punct:]]. Note the double brackets: [:digit:] is the class name, and it goes inside a bracket expression.

Basic vs extended regular expressions

grep's default dialect, BRE (basic), treats + ? | ( ) { } as ordinary characters; you must write \+, \{4\} and so on to make them special. That is awkward, so use grep -E (or the old name egrep) for ERE (extended), where they are special as written:

EREMeaning
x+one or more of x
x?zero or one of x (optional)
x{4}, x{2,5}, x{3,}exactly 4, from 2 to 5, at least 3
a|b (written a|b)a or b
( ... )grouping: (ab)+ is ab, abab, ...
$ grep -E 'WARN|ERROR' app.log | wc -l
7
$ grep -E 'user=(ana|ben)$' app.log | wc -l
6
$ grep -Eo '[0-9]+\.[0-9]+s' app.log
2.4s

The examples in this course use -E whenever a pattern needs more than the basics.

How matching works

grep does not "try the pattern at the start of the line" only. Conceptually it tries every starting position until one works. That is why ^ exists: without it, 2026 matches anywhere. Real implementations compile the regex into a state machine and scan each line once, which is why grep is so fast. Also: * and + are greedy (they match as much as possible), which matters with -o:

$ echo 'a="1" b="2"' | grep -o '".*"'
"1" b="2"
$ echo 'a="1" b="2"' | grep -o '"[^"]*"'
"1"
"2"

[^"]* ("any characters except a quote") is the standard trick for "up to the next quote".

Common mistakes

Exercises

Two tasks on app.log and a source tree, a quiz on the dot, and a filter script with an anchored extended regex.

Recap

Going further: man 7 regex describes both dialects in full. Try grep -E -o '[a-z]+=[^ ]+' app.log | sort | uniq -c once you know uniq (next lesson).

Exercises

Exercise 1 · task in a shell

Pull out the errors

Open the shell for this lesson. /work/app.log is a log from a small web application. Each line has a date, a time, a level (INFO, WARN or ERROR) and a message.

  1. Save every line with the level ERROR into /work/errors.txt (whole lines, in their original order). Careful: one INFO message contains the lowercase word error; it must not be included.
  2. Save the number of lines that are not INFO lines into /work/notinfo.txt.
Hint 1

grep ERROR app.log prints the matching lines. -v inverts the match and -c counts lines instead of printing them.

Hint 2

grep ERROR app.log > errors.txt and grep -vc INFO app.log > notinfo.txt

Reference solution
grep ERROR app.log > errors.txt
grep -vc INFO app.log > notinfo.txt

Exercise 2 · task in a shell

Which files mention TODO?

/work/src is a small source tree with files in several subdirectories. Save the names of the files under src that contain the text TODO into /work/todo.txt, one path per line (as grep prints them, e.g. src/lib/util.sh), sorted alphabetically with sort.

grep can search directories recursively and can print only file names instead of lines.

Hint 1

-r searches a directory recursively; -l prints only the names of files with a match.

Hint 2

grep -rl TODO src | sort > todo.txt. The sort makes the order independent of the directory order on disk.

Reference solution
grep -rl TODO src | sort > todo.txt

Exercise 3 · quiz

What does the dot match?

Which of these lines does grep 'a.c' print? Select all that apply.

Answer
  • abc

  • a.c

  • xxa-cxx

In a regular expression . matches any single character, so a.c means "a, any one character, c" anywhere in the line: abc, a.c and a-c inside xxa-cxx all match. ac has no character between a and c, and ABC is uppercase. To match a literal dot, escape it (a\.c) or use grep -F 'a.c', which turns off regex interpretation entirely.

Exercise 4 · write code

Keep only dated lines

Write a script that reads text from standard input and prints only the lines that start with a date in the form YYYY-MM-DD (four digits, dash, two digits, dash, two digits). Lines with a date somewhere else, or with a malformed date, are dropped.

Use one grep with an extended regular expression (-E). The script must exit with status 0 even when there are matching lines; if no line matches, grep exits with 1, which is fine here (the checker only uses input that has matches).

File: script.sh

Starting point

grep '2026'

Example

input

2026-10-30 backup done
backup on 2026-10-31 failed
2026-1-5 typo date
1999-12-31 party
no date at all

expected output

2026-10-30 backup done
1999-12-31 party

Example

input

2026-11-01T08:00 iso style
20261101 compact

expected output

2026-11-01T08:00 iso style
Hint 1

^ anchors the pattern to the start of the line. [0-9] is one digit and {4} (with -E) repeats it four times.

Hint 2

grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}'

Reference solution
grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}'