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/syslogWith no file argument, grep reads standard input, which is how it earns its place in pipelines:
ps aux | grep nginxAlways 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
| Flag | What it does |
|---|---|
-i | Case-insensitive match |
-r | Search directories recursively |
-n | Show line numbers |
-v | Invert — show lines that do not match |
-c | Print a count of matching lines instead of the lines |
-l | Print only the names of files containing a match |
-L | Print only the names of files with no match |
-w | Match whole words only |
-o | Print only the matched part, not the whole line |
-E | Extended regular expressions (same as egrep) |
-F | Fixed strings — no regex at all, and much faster |
-A n | Show n lines after each match |
-B n | Show n lines before each match |
-C n | Show n lines of context either side |
-q | Quiet — 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_configA 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.logThe 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 -rnPulls 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.
| Goal | Basic (default) | Extended (-E) |
|---|---|---|
| One or more | a\+ | a+ |
| Optional | a\? | a? |
| Alternation | cat\|dog | cat|dog |
| Grouping | \(ab\) | (ab) |
| Repetition | a\{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:
| Code | Meaning |
|---|---|
0 | At least one line matched |
1 | Nothing matched |
2 | An 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
egrepandfgrep— deprecated aliases forgrep -Eandgrep -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.
