Skip to content

grep Cheat Sheet: Patterns, Context, and Recursive Search

A dense GNU grep reference: every everyday flag, BRE vs ERE vs PCRE syntax, context lines, recursive search with include and exclude, exit codes, and tested one-liners.

●By 10 min read

Most people use about four grep flags and reach for a search engine for the rest. This page is the rest: a reference for GNU grep 3.x, the version on every current Debian, Ubuntu, RHEL and Fedora system, organised by what you are trying to do rather than alphabetically. Our Linux tools cheat sheet gives grep a few lines among dozens of tools; this one goes deep on just grep.

Every example below runs as written. Where BSD grep on macOS behaves differently, it is called out, and when a pattern gets complicated enough that you want to see what it matches before running it across a tree, paste it into the regex tester first.

The Everyday Flags

FlagLong formWhat it does
-i--ignore-caseCase-insensitive match
-v--invert-matchPrint lines that do NOT match
-n--line-numberPrefix each line with its line number
-c--countPrint the number of matching lines per file, not the lines
-l--files-with-matchesPrint only the names of files that contain a match
-L--files-without-matchPrint only the names of files with no match
-o--only-matchingPrint only the matched part, one match per line
-w--word-regexpMatch whole words only (letters, digits, underscore)
-x--line-regexpThe pattern must match the entire line
-m N--max-count=NStop reading a file after N matching lines
-H / -h--with-filename / --no-filenameForce or suppress the filename prefix
-q--quietNo output; exit 0 on first match (for scripts)
-s--no-messagesSuppress errors about missing or unreadable files
-e PAT--regexp=PATAdd a pattern; repeatable; safe for patterns starting with -
-f FILE--file=FILERead patterns from a file, one per line

$ grep -in 'permitrootlogin' /etc/ssh/sshd_config

33:#PermitRootLogin prohibit-password

$ grep -c ERROR /var/log/app/*.log

/var/log/app/api.log:14

/var/log/app/worker.log:0

# Several patterns at once (OR)

$ grep -e timeout -e refused -e reset app.log

# A pattern that starts with a dash

$ grep -e '-v' notes.txt

$ grep -- '--force' deploy.sh

-c counts lines, not matches β€” a line with three hits counts once. To count every occurrence, use grep -o pattern file | wc -l.

-w uses word characters β€” grep -w log matches log and log.txt but not login or syslog; the dot is a word boundary, the underscore is not.

Pattern Syntax: Basic, Extended, Fixed and Perl

grep has four matchers. The default is basic regular expressions (BRE), where several metacharacters need a backslash to be special. -E switches to extended (ERE), which is what most people expect. -F disables regex entirely, and -P enables Perl-compatible syntax.

FeatureBRE (default)ERE (-E)PCRE (-P)
One or morea\+a+a+
Zero or onea\?a?a?
Alternationa\|ba|ba|b
Group\(ab\)(ab)(ab)
Repeat 2 to 4a\{2,4\}a{2,4}a{2,4}
Digit[[:digit:]][[:digit:]]\d
Lazy quantifierNoNo.*?
Lookahead / lookbehindNoNo(?=x) (?<=x)
Reset match startNoNo\K

# Extended: lines that are blank or comments, inverted

$ grep -Ev '^\s*(#|$)' /etc/ssh/sshd_config

# Fixed strings: dots and brackets are literal, and it is faster

$ grep -F '[error] 10.0.0.1' error.log

# Repeated words ("the the") with a back-reference

$ grep -Eon '\b(\w+) \1\b' README.md

# Perl: print only the value after user=, without the key

$ grep -oP 'user=\K[^ ]+' auth.log

# Perl: a match that spans lines (-z reads the file as one record)

$ grep -Pzo '(?s)<VirtualHost[^>]*>.*?</VirtualHost>' site.conf | tr '\0' '\n'

Quote patterns in single quotes β€” otherwise the shell expands *, $ and backslashes before grep sees them. Double quotes only when you deliberately interpolate a variable.

GNU escapes work in BRE and ERE β€” \w \s \b \< \> are GNU extensions available without -P. They are not POSIX, so avoid them in scripts that must run on other systems.

-P is not everywhere β€” macOS ships BSD grep without -P. Use ggrep from Homebrew, or rewrite with -E. Some minimal containers also build grep without PCRE support.

egrep and fgrep are deprecated β€” since grep 3.8 they print an obsolescence warning on every call. Replace them with grep -E and grep -F in scripts.

Context Lines and Output Control

A match on its own rarely explains an error. Context flags print the lines around it, and grep marks which lines matched and which are context:

$ grep -n -B1 -A2 'Traceback' worker.log

211-2026-10-02 09:14:07 INFO job 8812 started

212:Traceback (most recent call last):

213- File "/srv/app/worker.py", line 88, in run

214- conn = pool.get(timeout=5)

--

540-2026-10-02 11:02:51 INFO job 9120 started

541:Traceback (most recent call last):

542- File "/srv/app/worker.py", line 88, in run

543- conn = pool.get(timeout=5)

FlagEffect
-A NN lines of trailing context after each match
-B NN lines of leading context before each match
-C NN lines on both sides (same as -A N -B N)
--group-separator=SEPReplace the -- printed between non-adjacent groups
--no-group-separatorPrint groups back to back with no separator
-bPrefix with the byte offset (of the match itself when combined with -o)
--color=autoHighlight matches when writing to a terminal
--color=alwaysKeep colour through a pipe, e.g. into less -R
--line-bufferedFlush every line; needed when grep sits mid-pipeline on a live stream
--label=NAMEFilename to show for standard input

Read the separators β€” in the output, : after the line number marks a matching line and - marks a context line. That is how you tell them apart when using -n or with multiple files.

-m with context β€” grep -m1 -A5 pattern stops after the first match but still prints its five trailing lines, which is a quick way to pull one complete stack trace.

Colours β€” set match colours with GREP_COLORS='mt=01;32'. The old GREP_COLOR variable is deprecated and triggers a warning on grep 3.8 and later.

sponsored

Recursive Search Across a Tree

With -r grep walks directories itself; with no file operand it searches the current directory. The filters below keep it out of directories you never want to search and away from binary files:

# Recursive, line numbers, skip binary files

$ grep -rnI 'DATABASE_URL' .

# Only certain file types (bash expands the braces into two --include flags)

$ grep -rn --include=*.{yml,yaml} 'image:' deploy/

# Skip noisy directories

$ grep -rn --exclude-dir={.git,node_modules,vendor} 'TODO' .

# Just the filenames, NUL-separated, safe to pass to xargs

$ grep -rlZ 'old.example.com' /etc/nginx/ | xargs -0 sed -i 's/old.example.com/new.example.com/g'

# Files that contain BOTH strings

$ grep -rlZ 'listen 443' /etc/nginx/ | xargs -0 grep -l 'http2'

# Combine with find for anything grep cannot filter on (age, size, owner)

$ find /var/log -name '*.log' -mtime -1 -print0 | xargs -0 grep -H 'OOM'

OptionMeaning
-rRecurse; follows symlinks only when they are named on the command line
-RRecurse and follow every symlink (can loop or leave the tree)
--include=GLOBSearch only files whose base name matches GLOB
--exclude=GLOBSkip files whose base name matches GLOB
--exclude-dir=GLOBDo not descend into matching directories
-IIgnore binary files (same as --binary-files=without-match)
-aTreat binary files as text
-ZWrite a NUL byte after each filename (pair with xargs -0)
-zInput records end in NUL instead of newline

Quote globs, except for brace tricks β€” --include='*.conf' stops the shell expanding the glob in the current directory. Brace lists are the exception: grep itself does not understand {a,b}, so leave them unquoted and let bash expand them.

Binary file matches β€” since grep 3.5 the "binary file matches" notice goes to standard error, so it no longer pollutes a pipeline. Older versions printed it on standard output. Use -I to skip such files, or -a to see the lines.

In a git repository β€” git grep searches tracked files only and honours .gitignore automatically, which is usually what you want in source trees.

Compressed logs β€” zgrep runs grep over gzip files and takes most of the same flags, e.g. zgrep -h 'Failed password' /var/log/auth.log.*.gz.

Exit Codes, Scripts and Speed

grep's exit status is designed for scripts, and getting it wrong is a common source of bugs under set -e:

Exit statusMeaning
0At least one line was selected
1No lines were selected
2An error occurred (missing file, bad regex), unless -q found a match

# Test for presence without printing anything

if grep -q '^PasswordAuthentication no' /etc/ssh/sshd_config; then

echo "password auth disabled"

fi

# Under set -e, a grep that finds nothing exits 1 and kills the script

count=$(grep -c 'ERROR' app.log || true)

# Distinguish "no match" from "real error"

grep -q 'pattern' file; rc=$?

[ "$rc" -eq 2 ] && { echo "grep failed" >&2; exit 2; }

# Set membership: lines in users.txt that are also in allowed.txt

$ grep -Fxf allowed.txt users.txt

# ... and the ones that are NOT

$ grep -vFxf allowed.txt users.txt

# Live filtering of a growing log (RHEL: /var/log/messages)

$ tail -F /var/log/syslog | grep --line-buffered -i 'error' | tee -a errors.txt

# Faster on huge files: byte-wise locale and fixed strings

$ LC_ALL=C grep -F 'session opened' /var/log/auth.log | wc -l

-q exits early β€” it stops at the first match, so it is also the fastest way to answer "does this file contain X?" on a large file. A pipeline upstream of it may then get SIGPIPE, which matters under pipefail.

LC_ALL=C β€” in a UTF-8 locale grep must decode multibyte characters; the C locale treats input as bytes. It can be much faster on large ASCII logs, but character classes then stop matching non-ASCII letters.

Prefer pgrep for processes β€” ps aux | grep nginx also matches the grep itself. pgrep -a nginx avoids that; the old trick is grep '[n]ginx'.

One-Liners Worth Keeping

# Top 10 client IPs in an nginx access log

$ grep -Eo '^[0-9]{1,3}(\.[0-9]{1,3}){3}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

# Failed SSH logins (Debian/Ubuntu: auth.log, RHEL/Fedora: secure)

$ grep 'Failed password' /var/log/auth.log | grep -oP 'from \K[0-9.]+' | sort | uniq -c | sort -rn

# Accounts with a login shell

$ grep -Ev ':(/usr)?/s?bin/(nologin|false)$' /etc/passwd | cut -d: -f1

# Active (non-comment) lines of every file in a config directory

$ grep -rhEv '^\s*(#|;|$)' /etc/php/*/fpm/pool.d/

# HTTP 5xx responses with the request line

$ grep -E '" 5[0-9]{2} ' /var/log/nginx/access.log | tail -20

# Which config file sets a directive, including commented-out copies

$ grep -rn --include=*.conf -i 'client_max_body_size' /etc/nginx/

# Lines longer than 200 characters

$ grep -nE '.{201,}' script.sh

# Show matches with filename, but only the first per file

$ grep -rnm1 'version' --include=package.json .

Logs on journald-only systems β€” minimal Debian 12 and newer installs may not run rsyslog, so /var/log/auth.log does not exist. Pipe the journal instead: journalctl -u ssh --no-pager | grep 'Failed password' (the unit is sshd on RHEL).

When grep is not enough β€” for field extraction and arithmetic, hand off to awk; our find, awk and sed one-liners guide covers the next step. The Linux commands reference lists related tools.

Frequently Asked Questions

What is the difference between grep -E and grep -P?

grep -E uses POSIX extended regular expressions, where +, ?, | and parentheses are special without backslashes. grep -P uses Perl-compatible regular expressions, which add \d, lazy quantifiers, lookahead and lookbehind, and \K. -E is available everywhere; -P is GNU-only and missing from BSD grep on macOS.

How do I grep recursively but only in certain file types?

Use grep -r with --include, for example grep -rn --include='*.py' 'import requests' . You can repeat --include, or use unquoted brace expansion such as --include=*.{yml,yaml}, which bash turns into two flags. Add --exclude-dir=.git to skip directories.

How do I show lines before and after a grep match?

Use -A N for lines after, -B N for lines before, or -C N for both. Non-adjacent groups are separated by a -- line, which you can change with --group-separator or remove with --no-group-separator. With -n, matching lines show a colon after the number and context lines show a dash.

Why does my script exit when grep finds nothing?

grep exits with status 1 when no lines match, and set -e treats any non-zero status as failure. Append || true where no match is acceptable, or capture $? and treat only status 2 as an error, since 2 means grep itself failed.

What is the difference between grep -r and grep -R?

Both search directories recursively. -r follows symbolic links only when they are given on the command line, while -R follows every symlink it encounters, which can search outside the intended tree or loop. Use -r unless you specifically need to follow links.

sponsored

Related Tools

See also