opscape

Text processing

grep

Print the lines of files (or standard input) that match a pattern.

grep [OPTION...] PATTERN [FILE...]

15 of 15 examples run in Debian 13.7 on 2026-10-31 · grep 3.11-4 · manual: grep(1)

grep reads text one line at a time and prints each line that contains a match for a pattern. It never edits anything; it filters. That makes it the first tool to reach for when the question is "where does this appear?" or "which lines mention that?", and the usual first stage of a pipeline that sort, uniq, awk or wc finish off.

The pattern is a basic regular expression by default, where +, ?, |, {} and () are literal characters. -E switches to extended syntax, where they are operators, and -F turns regex off entirely so . and * mean themselves, which is both faster and safer when you search for a literal string such as an IP address or a file name.

The exit status is part of the interface: 0 when at least one line matched, 1 when none did, 2 on an error such as a missing file. Scripts test it with if grep -q ...; then. When you need to change the matching lines rather than find them, use sed; when you need to pick fields out of them or do arithmetic, use awk.

Examples

Find lines containing a word

grep ERROR logs/app.log
explain
output
2026-03-14 08:16:40 ERROR health check failed: database timeout after 5000ms
2026-03-14 08:30:00 ERROR disk usage on /var at 91%

Every line with ERROR in it is printed in full. The match is case-sensitive and can be anywhere in the line, so ERRORS or MYERROR would match too.

Ignore upper and lower case

grep -i 'the' data/poem.txt
explain
output
The quick brown fox
jumps over the lazy dog.
The dog sleeps;
the fox runs on.

-i matches The and the alike, so all four lines of the poem match, including "the lazy dog" where the is lowercase in the middle of the line.

Show line numbers

grep -n fix todo.txt
explain
output
2:fix login bug
4:fix logout bug

-n prefixes each match with its line number, which is what you want before opening the file in an editor at that line.

Count matching lines

grep -c WARN logs/app.log
explain
output
4

-c prints the number of matching lines, not the number of matches: a line with WARN twice still counts once. To count every occurrence, use grep -o WARN file | wc -l.

Show the lines that do not match

grep -v -e '^#' -e '^$' data/settings.conf
explain
output
host = 0.0.0.0
port = 8080
log_level = info
log_file = /var/log/demo/app.log
timeout = 30

-v inverts the match. With two -e patterns a line is dropped if it matches either one: ^# is a comment line, ^$ an empty line. What remains is the effective configuration. The commented-out # max_connections setting is gone because it starts with #.

Search a directory tree

grep -rn TODO .
explain
output
./src/lib/helpers.py:2:    # TODO: handle unicode
./src/app.py:11:    # TODO: read the port from the environment
./notes.txt:5:- TODO: write the deploy script

-r descends into every directory under . and prefixes each match with its file name; -n adds line numbers. It finds the TODOs in the notes and in both Python files. -r does not follow symlinks it meets inside the tree; -R does.

Search only some file types

grep -rn --include='*.py' TODO .
explain
output
./src/lib/helpers.py:2:    # TODO: handle unicode
./src/app.py:11:    # TODO: read the port from the environment

--include limits a recursive search to file names matching a glob. Quote the glob so the shell does not expand it first. --exclude and --exclude-dir (for example --exclude-dir=.git) do the opposite.

List only the names of matching files

grep -rl 'database' .
explain
output
./data/config.json
./data/servers.json
./logs/error.log
./logs/app.log

-l prints each matching file once and stops reading it at the first match, which makes it fast on big trees. -L lists the files that do not match.

Match whole words only

echo '--- without -w'; grep user logs/app.log
echo '--- with -w'; grep -w user logs/app.log
output
--- without -w
2026-03-14 08:03:45 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:03:46 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:05:09 INFO  user lookup id=7 not found
2026-03-14 08:11:47 WARN  permission denied: DELETE /api/users/3
2026-03-14 08:25:00 INFO  user id=2 updated by api
--- with -w
2026-03-14 08:03:45 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:03:46 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:05:09 INFO  user lookup id=7 not found
2026-03-14 08:25:00 INFO  user id=2 updated by api

-w requires the match to stand alone, bounded by characters other than letters, digits and _. Without it, user also matches inside /api/users/3. With it, that line drops out, while user=alice still matches because = is a word boundary.

Print only the matched text

grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' logs/app.log
explain
output
198.51.100.7
198.51.100.7
198.51.100.23

-o prints just the part of the line that matched, one match per output line. The extended regex (-E) is a dotted quad: one to three digits, then three groups of a dot and one to three digits. The two 198.51.100.7 lines show it is per occurrence, not unique.

Match one of several patterns

grep -E 'WARN|ERROR' logs/app.log
explain
output
2026-03-14 08:03:45 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:03:46 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:11:47 WARN  permission denied: DELETE /api/users/3
2026-03-14 08:16:40 ERROR health check failed: database timeout after 5000ms
2026-03-14 08:20:12 WARN  blocked request for /.env from 198.51.100.23
2026-03-14 08:30:00 ERROR disk usage on /var at 91%

In extended syntax | means "or". The basic-regex spelling is grep 'WARN\|ERROR' (a GNU extension); grep -e WARN -e ERROR is the portable way to give several patterns.

Show lines around each match

grep -n -B1 -A2 'health check failed' logs/app.log
explain
output
6-2026-03-14 08:11:47 WARN  permission denied: DELETE /api/users/3
7:2026-03-14 08:16:40 ERROR health check failed: database timeout after 5000ms
8-2026-03-14 08:16:50 INFO  health check recovered
9-2026-03-14 08:20:12 WARN  blocked request for /.env from 198.51.100.23

-B1 adds one line before the match and -A2 two lines after. -C N does both at once. In the output, matching lines use : after the line number and context lines use -. When separate groups of context do not touch, grep prints -- between them.

Search for a literal string with dots

grep -c '10.0.0.1' data/hosts.txt
grep -cF '10.0.0.1' data/hosts.txt
grep -cw -F '10.0.0.1' data/hosts.txt
outputexit 1
2
2
0

As a regex, . matches any character and 10.0.0.1 is a substring of 10.0.0.11 and 10.0.0.12, so the first two counts are 2. -F makes the dots literal, but substring matching still finds both lines. Only -F plus -w asks for that exact address, and no host has it, so the last count is 0 and that command's exit status is 1.

Test for a match in a script

if grep -q 'DEBUG=true' .env; then echo "debug is on"; else echo "debug is off"; fi
grep -q CRITICAL logs/app.log; echo "exit status: $?"
grep -q x missing.txt; echo "exit status: $?"
output
debug is off
exit status: 1
grep: missing.txt: No such file or directory
exit status: 2

-q prints nothing and exits on the first match, so only the exit status speaks: 0 for a match, 1 for none, 2 for an error such as the missing file, whose message still goes to stderr. Use -s to silence that message.

Count requests per status code

grep -oE 'HTTP/1.1" [0-9]{3}' access.log | cut -d' ' -f2 | sort | uniq -c | sort -rn
explain
output
     11 200
      4 404
      2 403
      2 401
      1 500

The pattern anchors on the protocol and closing quote of the request, so the three digits after it are the status code and never the byte count. cut keeps the digits, sort | uniq -c counts each distinct status and sort -rn puts the most common first.

Common pitfalls

  • grep -c counts lines, not occurrences. A line with three matches adds one. Use grep -o PATTERN | wc -l to count occurrences.

  • A pattern that starts with - is read as an option: grep -v file does not search for -v. Write grep -e -v file or grep -- -v file.

  • grep pattern * skips subdirectories (printing Is a directory for each) and misses dotfiles such as .env. Use grep -r pattern . instead.

  • In a pipeline under set -o pipefail, grep finding nothing returns 1 and fails the whole pipeline. Add || true after grep when no match is a normal outcome.

  • Unquoted patterns are expanded by the shell first: grep *.log turns into a list of file names. Always single-quote the pattern.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • -P (Perl-compatible regex) exists in GNU grep but not in the grep that ships with macOS; use -E for portability.

  • \|, \+ and \? in basic regex are GNU extensions. With BSD grep use -E and write |, +, ?.

  • Ubuntu and Debian ship GNU grep; macOS ships a BSD-derived grep that supports -r, -o, -w, --include and the context options, so everyday use is the same.

Options worth knowing

A selection, described in our own words. For the full list, see grep(1) or grep --help.

OptionWhat it does
-i, --ignore-caseMatch upper and lower case alike.
-v, --invert-matchSelect the lines that do not match.
-c, --countPrint only a count of matching lines per file.
-n, --line-numberPrefix each output line with its line number.
-l, --files-with-matchesPrint only the names of files with at least one match.
-L, --files-without-matchPrint only the names of files with no match.
-o, --only-matchingPrint only the matched parts, one per line.
-w, --word-regexpMatch only whole words.
-x, --line-regexpMatch only whole lines.
-E, --extended-regexpUse extended regular expressions (| + ? {} () are operators).
-F, --fixed-stringsTreat the pattern as a literal string, not a regex.
-P, --perl-regexpUse Perl-compatible regular expressions.
-e, --regexp PATTERNUse PATTERN; repeatable, and safe for patterns that start with -.
-f, --file FILERead patterns from FILE, one per line.
-r, --recursiveSearch directories recursively, not following symlinks inside them.
-R, --dereference-recursiveSearch recursively and follow all symlinks.
--include GLOBIn a recursive search, only read files whose name matches GLOB.
--exclude GLOBSkip files whose name matches GLOB.
--exclude-dir GLOBSkip directories whose name matches GLOB.
-A, --after-context NUMAlso print NUM lines after each match.
-B, --before-context NUMAlso print NUM lines before each match.
-C, --context NUMPrint NUM lines before and after each match.
-q, --quietPrint nothing; exit 0 on the first match.
-s, --no-messagesSuppress error messages about missing or unreadable files.
-m, --max-count NUMStop reading a file after NUM matching lines.
-h, --no-filenameDo not prefix matches with the file name.
-H, --with-filenameAlways prefix matches with the file name.
-z, --null-dataTreat input as NUL-separated records instead of lines.
-Z, --nullFollow each output file name with a NUL byte, for xargs -0.
--color[=WHEN]Highlight matches: never, always or auto.

Related