grep searches text for lines matching a pattern and prints them. That one sentence covers most of what it does, but the reason it is on nearly every Linux system and in nearly every shell pipeline is that “text” on Linux means almost everything — log files, config files, source code, the output of any other command.

The name comes from an old ed editor command, g/re/p: globally search for a regular expression and print.

Basic syntax

grep [options] PATTERN [file...]

Find every line containing the word error in a log file:

grep error /var/log/syslog

With no file argument, grep reads standard input, which is how it earns its place in pipelines:

ps aux | grep nginx

Always quote your pattern. The shell expands *, ?, $ and friends before grep ever sees them, which produces baffling results. Single quotes are safest: grep 'error.*timeout' file.log.

The flags worth memorising

FlagWhat it does
-iCase-insensitive match
-rSearch directories recursively
-nShow line numbers
-vInvert — show lines that do not match
-cPrint a count of matching lines instead of the lines
-lPrint only the names of files containing a match
-LPrint only the names of files with no match
-wMatch whole words only
-oPrint only the matched part, not the whole line
-EExtended regular expressions (same as egrep)
-FFixed strings — no regex at all, and much faster
-A nShow n lines after each match
-B nShow n lines before each match
-C nShow n lines of context either side
-qQuiet — print nothing, just set the exit code

Flags combine in the usual way, so grep -rin is recursive, case-insensitive, with line numbers — probably the single most useful invocation there is.

Recipes you will actually use

Find which file defines something

grep -rn "DATABASE_URL" /etc/myapp/

Recursive with line numbers, so you get path/to/file:42:DATABASE_URL=... and can jump straight there.

Strip comments and blank lines from a config file

grep -v -e '^#' -e '^$' /etc/ssh/sshd_config

A 200-line config file usually has about fifteen lines that actually do anything. -e lets you supply more than one pattern, and -v inverts the whole thing.

See what happened around an error

grep -C 5 -i "fatal" /var/log/app.log

The line that says “fatal” is rarely the interesting one. The five lines before it usually are.

Extract just the matches

grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log | sort | uniq -c | sort -rn

Pulls every IP address out of a log, counts them, and ranks by frequency. -o is what makes this possible — without it you would get whole log lines instead of the addresses.

Search only certain file types

grep -rn --include="*.py" "import requests" .

--include and --exclude take glob patterns; --exclude-dir=node_modules is the one that saves your sanity in a JavaScript project.

Regular expressions: basic vs extended

This trips up almost everyone. By default grep uses basic regular expressions (BRE), where +, ?, {}, () and | are literal characters and must be backslash-escaped to act as operators. With -E you get extended regular expressions (ERE), where they work the way you expect from every other language.

GoalBasic (default)Extended (-E)
One or morea\+a+
Optionala\?a?
Alternationcat\|dogcat|dog
Grouping\(ab\)(ab)
Repetitiona\{2,4\}a{2,4}

The practical advice: just use -E whenever your pattern is more than a plain string. It costs nothing and removes a whole category of confusion.

Useful character classes work in both modes: [[:digit:]], [[:alpha:]], [[:space:]], [[:alnum:]]. These are locale-aware, unlike [0-9].

Exit codes and scripting

grep tells you whether it found anything through its exit status, which makes it useful in conditionals:

CodeMeaning
0At least one line matched
1Nothing matched
2An error occurred (file missing, bad pattern)
if grep -q "PermitRootLogin yes" /etc/ssh/sshd_config; then
    echo "Root SSH login is enabled — you probably want to fix that"
fi

-q suppresses output entirely and exits as soon as it finds the first match, which also makes it fast on large files.

One consequence worth knowing: because “no match” is exit code 1, a grep that finds nothing will cause a script running under set -e to abort. Guard it with || true when an empty result is acceptable.

Gotchas

grep finds its own process

ps aux | grep nginx always shows an extra line: the grep command itself, which contains the word “nginx”. The traditional fix is a character class that does not match itself:

ps aux | grep '[n]ginx'

The pattern [n]ginx matches the string “nginx”, but the literal text in the process list is [n]ginx, which does not match. Clever, slightly horrible, universally used. pgrep nginx is the sane modern answer.

Binary files

If grep decides a file is binary it prints “Binary file X matches” instead of the line. Use -a to treat it as text, or -I to skip binary files entirely during a recursive search.

Performance

If your pattern is a plain string with no regex metacharacters, -F can be dramatically faster. Setting LC_ALL=C also speeds things up considerably by skipping UTF-8 handling — safe when you are searching ASCII data.

For searching large codebases, ripgrep (rg) is significantly faster and respects .gitignore by default. It is not installed by default on most distributions, but it is worth adding.

Related commands

  • egrep and fgrep — deprecated aliases for grep -E and grep -F. Modern grep warns when you use them.
  • zgrep — searches gzipped files without unpacking them first, which is exactly what you want for rotated logs.
  • sed — when you need to change matching lines rather than just find them.
  • awk — when you need to act on specific fields within matching lines.
  • find — searches by filename and metadata rather than content; often piped into grep.