Text processing
grep
Print the lines of files (or standard input) that match a pattern.
grep [OPTION...] PATTERN [FILE...]
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
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
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
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
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
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 .
./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 .
./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' .
./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
--- 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
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
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
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
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: $?"
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
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 -ccounts lines, not occurrences. A line with three matches adds one. Usegrep -o PATTERN | wc -lto count occurrences.A pattern that starts with
-is read as an option:grep -v filedoes not search for-v. Writegrep -e -v fileorgrep -- -v file.grep pattern *skips subdirectories (printingIs a directoryfor each) and misses dotfiles such as.env. Usegrep -r pattern .instead.In a pipeline under
set -o pipefail,grepfinding nothing returns 1 and fails the whole pipeline. Add|| trueafter grep when no match is a normal outcome.Unquoted patterns are expanded by the shell first:
grep *.logturns 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-Efor portability.\|,\+and\?in basic regex are GNU extensions. With BSD grep use-Eand write|,+,?.Ubuntu and Debian ship GNU grep; macOS ships a BSD-derived grep that supports
-r,-o,-w,--includeand 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.
| Option | What it does |
|---|---|
-i, --ignore-case | Match upper and lower case alike. |
-v, --invert-match | Select the lines that do not match. |
-c, --count | Print only a count of matching lines per file. |
-n, --line-number | Prefix each output line with its line number. |
-l, --files-with-matches | Print only the names of files with at least one match. |
-L, --files-without-match | Print only the names of files with no match. |
-o, --only-matching | Print only the matched parts, one per line. |
-w, --word-regexp | Match only whole words. |
-x, --line-regexp | Match only whole lines. |
-E, --extended-regexp | Use extended regular expressions (| + ? {} () are operators). |
-F, --fixed-strings | Treat the pattern as a literal string, not a regex. |
-P, --perl-regexp | Use Perl-compatible regular expressions. |
-e, --regexp PATTERN | Use PATTERN; repeatable, and safe for patterns that start with -. |
-f, --file FILE | Read patterns from FILE, one per line. |
-r, --recursive | Search directories recursively, not following symlinks inside them. |
-R, --dereference-recursive | Search recursively and follow all symlinks. |
--include GLOB | In a recursive search, only read files whose name matches GLOB. |
--exclude GLOB | Skip files whose name matches GLOB. |
--exclude-dir GLOB | Skip directories whose name matches GLOB. |
-A, --after-context NUM | Also print NUM lines after each match. |
-B, --before-context NUM | Also print NUM lines before each match. |
-C, --context NUM | Print NUM lines before and after each match. |
-q, --quiet | Print nothing; exit 0 on the first match. |
-s, --no-messages | Suppress error messages about missing or unreadable files. |
-m, --max-count NUM | Stop reading a file after NUM matching lines. |
-h, --no-filename | Do not prefix matches with the file name. |
-H, --with-filename | Always prefix matches with the file name. |
-z, --null-data | Treat input as NUL-separated records instead of lines. |
-Z, --null | Follow each output file name with a NUL byte, for xargs -0. |
--color[=WHEN] | Highlight matches: never, always or auto. |
Related
sedEdit a stream of text with a script of commands, most often to substitute text or delete lines.awkScan text as records and fields, and run a small program on each line that matches a pattern.findWalk a directory tree and print, or act on, every file that matches a set of tests.xargsRead items from standard input and run a command with them as arguments, in batches or in parallel.sortSort lines of text alphabetically, numerically, by version or by chosen fields; also merge and de-duplicate.uniqCollapse or report repeated adjacent lines; with sort, count how often each line occurs.wcCount lines, words, characters and bytes in files or standard input.- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.
- Pipes and pipelinesHow cmd1 | cmd2 connects processes, which exit status a pipeline returns, PIPESTATUS, pipefail, SIGPIPE, and why variables set in a piped loop vanish.
- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.