grep

Search text with patterns

Updated 2026-07-05

grep searches its input, one or more files, or whatever's piped to it, for lines that match a pattern, and prints those lines. It's the tool you reach for whenever you know roughly what you're looking for but not where it lives: which log line mentioned the failed request, which source file still has a TODO, which config line sets the timeout.

The mental model: one line in, one line out

grep reads its input line by line. For each line, it tests the pattern; if the line matches, it prints the whole line (by default) and moves on. It never looks across lines unless you explicitly ask it to (-A/-B/-C for context, -z for NUL-separated input). This is why grep is fast on huge files and why it composes so naturally in a pipeline. Like every classic Unix tool, it does one thing to a stream and lets the next command in the pipeline do the rest.

Two numbers matter when you're using grep in a script rather than reading its output yourself: exit status (0 if something matched, 1 if nothing matched, 2 on an actual error) and line count (-c). Both let you skip printing anything at all; see the scripting section below.

Three regex flavours, one command

grep's biggest source of confusion is that it supports three different pattern languages, selected by flag:

  • Basic regular expressions (BRE), the default. +, ?, |, and () are literal characters unless you backslash-escape them (\+, \?, \|, \(\)).
  • Extended regular expressions (ERE), enabled with -E (or run egrep, which is equivalent but deprecated). +, ?, |, and () work the way you'd expect from most other languages, unescaped. If you're writing anything beyond the simplest literal search, reach for -E. It's one flag and it saves you from a backslash minefield.
  • Fixed strings, enabled with -F (or run fgrep). No regex at all: every character in the pattern is literal. Use this when your search term might contain regex metacharacters you don't want interpreted (a literal ., [, or *). It's also measurably faster on very large inputs, since there's no regex engine involved.

GNU grep adds a fourth, non-POSIX option: -P for Perl-compatible regular expressions, which unlocks lookahead/lookbehind ((?<=...), (?=...)) and other features BRE/ERE don't have. It's not portable to non-GNU greps (BSD, busybox), so treat it as a last resort for things ERE genuinely can't express.

What "matching" actually prints

By default grep prints the whole matching line, not just the matched text. This surprises people used to regex functions in programming languages that return only the match. To get just the matched substring, add -o. To get neither the line nor the match, just a count or a yes/no, use -c or -q.

When you search multiple files, grep prefixes each line with the filename automatically (-H forces this even for a single file; -h suppresses it even for multiple files). This is why grep -r "TODO" . output is more useful than piping find through a single-file grep would be.

grep vs sed vs awk

All three read text line by line, but they answer different questions. grep answers "which lines match?" and prints lines verbatim. sed answers "how do I transform matching lines?" awk answers "how do I pull fields out of matching lines and compute something?" A common pattern is chaining them: grep to find the relevant lines, then awk or cut to pull out a field, then sort | uniq -c to tally it. See Pipes and redirection for why that composition works.

A note on performance

For simple literal searches over large files, -F is faster than a regex search, and setting LC_ALL=C before a search on ASCII text can noticeably speed up both -F and regex matching. GNU grep's regex engine does extra work to handle multi-byte locales that a byte-oriented C locale skips entirely.

Basic matching

Plain-text searches, the bread and butter.

Search a file for a string

grep "error" app.log

Prints every line containing "error". Quoting the pattern avoids surprises with shell metacharacters.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Search case-insensitively

grep -i "warning" app.log

`-i` (`--ignore-case`) matches "Warning", "WARNING", and "warning" alike.

Show output
Jul  5 09:14:23 deb1 app[312]: warning: retrying in 5s

Invert the match

grep -v "^#" config.conf

`-v` (`--invert-match`) prints lines that do NOT match the pattern: here, everything except comment lines.

Show output
listen 8080
workers 4
timeout 30

Search from standard input

cat config.conf | grep "listen"

`grep` reads stdin when no file is given, so it slots into any pipeline, though here `cat config.conf | grep listen` could just as well be `grep listen config.conf`. Keep `cat` when the input already came from an earlier pipe stage.

Show output
listen 8080

Counting and yes/no checks

Sometimes you don't want the lines, just a number or an exit code.

Count non-blank lines

grep -c "^$" config.conf

An empty-pattern-for-blank-lines trick: `^$` matches only lines with nothing between start and end. Here it's `0`, since the file has no blank lines.

Show output
0

Check for a match without printing anything

if grep -q "error" app.log; then
  echo "errors found"
fi

`-q` (`--quiet`) suppresses all output and just sets the exit status. This is `grep`'s most common use inside `if` conditions in scripts.

Show output
errors found

Use grep's exit code directly

grep -q "error" app.log; echo $?
grep -q "nonexistent" app.log; echo $?

`grep` exits `0` when it finds at least one match and `1` when it finds none. That's the exit code `if grep -q ...` is actually testing.

Show output
0
1

Tell a real error apart from a plain no-match

grep "error" no-such-file.log
echo "exit=$?"

Exit status `2` means something actually went wrong (missing file, bad option), different from the `1` a clean no-match returns. A script that only checks `$? -ne 0` treats both the same; check for `2` specifically if you need to tell them apart.

Show output
grep: no-such-file.log: No such file or directory
exit=2

Searching directories

Point grep at a whole tree instead of a single file.

Search recursively through a directory

grep -r "TODO" ~/projects

`-r` (`--recursive`) walks every file under the directory, printing matching lines prefixed with the file path.

Show output
projects/app/src/main.ts:// TODO: refactor this
projects/app/README.md:TODO: write docs

Search only specific file types

grep -r --include="*.ts" "TODO" ~/projects

Restricts a recursive search to files matching a glob, so you don't wade through generated files or docs.

Show output
projects/app/src/main.ts:// TODO: refactor this

Show the file and line number of each match

grep -rn "TODO" ~/projects

`-n` (`--line-number`) prefixes each match with its line number, `-r` makes it recursive.

Show output
projects/app/README.md:2:TODO: write docs
projects/app/src/main.ts:1:// TODO: refactor this

Exclude a subdirectory from a recursive search

grep --exclude-dir=src -r "TODO" ~/projects

`--exclude-dir` skips whole directories by name, useful for `node_modules`, `.git`, or in this case a `src/` tree you've already checked separately.

Show output
projects/app/README.md:TODO: write docs

Exclude files by glob

grep --exclude="*.md" -r "TODO" ~/projects

The file-level counterpart to `--exclude-dir`: skip anything matching the glob.

Show output
projects/app/src/main.ts:// TODO: refactor this

Count matches per file across a whole tree

grep -rc "TODO" ~/projects

Combine `-r` with `-c` to get a per-file tally, including `0` for files that don't match. Useful for spotting which files in a batch still need attention.

Show output
projects/app/src/main.ts:1
projects/app/src/util.ts:0
projects/app/README.md:1

Regex: extended patterns and character classes

Extended regex syntax, anchors, and character classes.

Use extended regular expressions

grep -E "error|warning" app.log

`-E` (`--extended-regexp`) enables extended regex syntax, so `|` means alternation without backslash-escaping it.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:23 deb1 app[312]: warning: retrying in 5s
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Alternation in basic regex needs escaping

grep "error\|warning" app.log

Without `-E`, `|` is a literal pipe character in GNU grep's BRE mode unless you escape it as `\|`. This line is equivalent to the `-E` version above, just harder to read.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:23 deb1 app[312]: warning: retrying in 5s
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Anchor a match to the start of the line

grep "^listen" config.conf

`^` anchors the pattern to the beginning of the line, so this won't match `# listen` (commented out).

Show output
listen 8080

Match a range of digits with a quantifier

grep -E "4[0-9]{2}" access.log

`{2}` repeats the preceding character class exactly twice. Here it matches any 4xx HTTP status code.

Show output
198.51.100.7 - - [05/Jul/2026:09:14:10 +0000] "POST /login HTTP/1.1" 401 128
198.51.100.7 - - [05/Jul/2026:09:14:12 +0000] "POST /login HTTP/1.1" 401 128

Use POSIX character classes

grep -oE "[[:alpha:]]+@[[:alpha:]]+" <<< "contact user@example not a real addr"

`[[:alpha:]]` is a locale-aware POSIX character class (letters only), more portable than `[a-zA-Z]` when the input might not be plain ASCII.

Show output
user@example

Make a character optional

grep -E "colou?r" <<< "color and colour"

`?` makes the preceding character optional, matching both American and British spelling in one pattern.

Show output
color and colour

The same optional-character match in basic regex

grep "colou\?r" <<< "color and colour"

In BRE, `?` is literal unless escaped as `\?`. This is the same pattern as the `-E` version above, spelled the BRE way.

Show output
color and colour

Extract IP addresses from a log

grep -oE "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" access.log | sort -u

`-o` prints only the matched text, not the whole line; piping through `sort -u` gives a unique list of client IPs that hit the server.

Show output
198.51.100.7
203.0.113.5
203.0.113.9

Context lines

Show the lines around a match, not just the match itself.

Show lines of context around a match

grep -A 3 -B 1 "Traceback" app.log

`-A 3` (`--after-context`) prints 3 lines after each match, `-B 1` (`--before-context`) prints 1 line before. Useful for reading a stack trace in context.

Show output
Jul  5 09:14:33 deb1 app[312]: info: connected
Jul  5 09:15:01 deb1 app[313]: Traceback (most recent call last):
Jul  5 09:15:01 deb1 app[313]:   File "worker.py", line 42, in run
Jul  5 09:15:01 deb1 app[313]:     result = process(item)
Jul  5 09:15:01 deb1 app[313]: ValueError: invalid item

Show the same number of lines before and after

grep -C 2 "Traceback" app.log

`-C` (`--context`) is shorthand for equal `-A`/`-B` values.

Show output
Jul  5 09:14:28 deb1 app[312]: error: connection refused
Jul  5 09:14:33 deb1 app[312]: info: connected
Jul  5 09:15:01 deb1 app[313]: Traceback (most recent call last):
Jul  5 09:15:01 deb1 app[313]:   File "worker.py", line 42, in run
Jul  5 09:15:01 deb1 app[313]:     result = process(item)

Whole-word and whole-line matching

Avoid matching a pattern as a substring of a longer word.

Match whole words only

grep -w "log" notes.txt

`-w` (`--word-regexp`) requires a word boundary on both sides, so it matches standalone "log" but skips "login", "logged", and "catalogue".

Show output
See the log for details.

Match a whole line exactly

grep -x "workers 4" config.conf

`-x` (`--line-regexp`) requires the pattern to match the entire line, not just part of it.

Show output
workers 4

Drop blank lines from output

grep -v "^$" notes.txt

A common cleanup step before piping text into something that doesn't expect blank lines.

Show output
Meeting notes
The login system needs work.
Login attempts are logged separately.
catalogue entries pending review
See the log for details.

Output control

Controlling exactly what grep prints, beyond the matching line.

Print only the first N matches

grep -m 1 "error" app.log

`-m` (`--max-count`) stops after N matches. Useful for a quick peek at a huge file.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused

List filenames that contain a match

grep -l "error" *.log

`-l` (`--files-with-matches`) prints filenames only, once each, instead of matching lines. Handy for `grep -l ... | xargs`.

Show output
app.log

Suppress filenames when searching multiple files

grep -h "error" app.log app.log

`-h` (`--no-filename`) drops the `filename:` prefix grep normally adds once you pass it more than one file.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:28 deb1 app[312]: error: connection refused
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Force filenames even for a single file

grep -H "workers" config.conf

`-H` (`--with-filename`) is the inverse of `-h`, forcing the prefix even when there's only one file. Useful in scripts that always expect it.

Show output
config.conf:# workers = number of worker processes
config.conf:workers 4

Count matches per file

grep -c "error" app.log access.log

With multiple files, `-c` prints one count per file, prefixed with the filename, including `0` for files with no match.

Show output
app.log:2
access.log:0

Multiple patterns at once

Search for several patterns in a single pass.

Match either of two patterns

grep -e "error" -e "warning" app.log

Repeat `-e` (`--regexp`) once per pattern. Equivalent to `-E "error|warning"` but handy when patterns are built up programmatically.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:23 deb1 app[312]: warning: retrying in 5s
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Read patterns from a file

printf 'error\nwarning\n' > patterns.txt
grep -f patterns.txt app.log

`-f` (`--file`) reads one pattern per line from a file. Useful for a long or frequently-updated pattern list.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:23 deb1 app[312]: warning: retrying in 5s
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Fixed strings vs regex

When you want a literal search, not a pattern match.

Search for a fixed string

grep -F "203.0.113.5" access.log

`-F` (`--fixed-strings`) treats the pattern as literal text. No regex engine is involved, so a `.` in an IP address matches only a literal dot.

Show output
203.0.113.5 - - [05/Jul/2026:09:12:01 +0000] "GET /index.html HTTP/1.1" 200 1024
203.0.113.5 - - [05/Jul/2026:09:12:03 +0000] "GET /style.css HTTP/1.1" 200 512

Search for a string that isn't there

grep -F "192.168.1.1" access.log; echo "exit=$?"

No match means no output and exit status `1`. This is what a script's `if grep -q ...` check is actually branching on.

Show output
exit=1

Binary and compressed files

grep's behaviour on data that isn't plain text.

Search inside a file grep thinks is binary

grep -a "binary" binary.bin

By default `grep` prints just "binary file matches" for files it detects as binary. `-a` (`--text`) forces it to treat the file as text and print the actual matching line.

Show output
binarydata

Search a gzip-compressed file directly

zgrep "error" app.log.gz

`zgrep` is a wrapper that decompresses on the fly and runs `grep`, no need to `gunzip` first. `zcat file | grep ...` is the same idea spelled out.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Using grep in scripts and pipelines

Patterns that come up once grep is one step in something bigger.

Feed a stream of found files into grep

find . -name "*.ts" -print0 | xargs -0 grep -l "TODO"

`find -print0` and `xargs -0` use NUL-separated names instead of newlines, so this survives filenames with spaces. See [find](/commands/find/) for more on `-print0`.

Show output
./projects/app/src/main.ts

Tally the most frequent value in a field

awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -3

`grep` alone can't group and count. Chain it with `awk` (or `cut`) to pull a field, then `sort | uniq -c` to tally, then `sort -rn` to rank.

Show output
3 198.51.100.7
2 203.0.113.9
2 203.0.113.5

Grep a live, growing log file

tail -f app.log | grep --line-buffered "error"

Without `--line-buffered`, grep's output can lag behind `tail -f` because it buffers output in blocks when it isn't writing straight to a terminal (e.g. into another pipe). `--line-buffered` flushes after every line.

Find lines in one file that are missing from another

grep -vFf allowed-users.txt current-users.txt

`-v` plus `-F -f` turns grep into a quick diff-by-line tool: print every line of `current-users.txt` that does NOT appear anywhere in `allowed-users.txt`. Handy for spotting drift between two lists without reaching for `diff`, which cares about line order.

Show output
alice
carol

Speed up a large literal search

LC_ALL=C grep -F "error" app.log

Setting `LC_ALL=C` switches to a byte-oriented locale, which can measurably speed up `grep` on large ASCII files by skipping multi-byte character handling.

Show output
Jul  5 09:14:22 deb1 app[312]: error: connection refused
Jul  5 09:14:28 deb1 app[312]: error: connection refused

Perl-compatible regex (GNU extension)

-P unlocks lookahead and lookbehind, at the cost of portability.

Extract a number using lookbehind/lookahead

grep -oP "(?<=\[)\d+(?=\])" app.log

`-P` (`--perl-regexp`) enables PCRE syntax. `(?<=\[)` and `(?=\])` are zero-width lookbehind/lookahead: they require the brackets to be there without including them in the match. Not portable to BSD/busybox grep.

Show output
312
312
312
312
312
313
313
313
313

Match only if a pattern does NOT appear on the line

grep -P "^(?!.*error).*warning" app.log

Negative lookahead (`(?!...)`) has no equivalent in BRE/ERE. This is the main reason to reach for `-P` at all. Here it matches lines with "warning" but not "error".

Show output
Jul  5 09:14:23 deb1 app[312]: warning: retrying in 5s