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
| Option | Effect |
|---|---|
-i | ignore case: error matches ERROR and Error |
-v | invert: print lines that do not match |
-c | print the count of matching lines instead of the lines |
-n | prefix each line with its line number |
-w | match whole words only: -w cat does not match concat |
-o | print only the matched part, one per line |
-l | print only names of files that contain a match |
-r | search directories recursively |
-q | quiet: print nothing, just set the exit status |
-E | extended regular expressions (below) |
-F | fixed string: no regex at all, every character is literal |
-A N, -B N, -C N | also 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):
| Regex | Matches |
|---|---|
. | 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:
| ERE | Meaning |
|---|---|
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
- Not quoting the pattern.
grep [0-9]* filelets bash expand the glob first; if files match, grep receives file names as the pattern. Alwaysgrep '...'. .meaning a literal dot.grep 1.2.3also matches1x2y3. Write'1\.2\.3'or use-F.+,?,{}without-E.grep '[0-9]+'searches for a digit followed by a literal+.- Case. grep is case-sensitive;
-iwidens the match, which may be more than you want. - Parsing with
grep -cand expecting the number of matches.-ccounts lines; a line with three matches counts once. Usegrep -o ... | wc -lto count occurrences. grep -rwithout-Ion binary files printsbinary file matches. Add-Ito skip binaries.
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
grep PATTERN filesprints matching lines; without files it filters stdin.- Key options:
-i -v -c -n -w -o -l -r -q -E -F. - Exit status: 0 match, 1 no match, 2 error;
-qfor silent tests. - Regex basics:
.[...][^...]^$*; with-Ealso+ ? {n,m} | ( ). - Quote patterns, and escape a dot you mean literally.
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).