Short answer: grep PATTERN FILE prints every line in FILE that matches PATTERN. Pipe anything into it to filter command output the same way: ps aux | grep nginx, journalctl | grep error. The flags people reach for constantly are -i (ignore case), -v (invert — show lines that don’t match), -r (recurse into a directory) and -n (show the line number).
grep nginx /etc/services # lines containing "nginx"
ps aux | grep nginx # filter a command's output
grep -rn "TODO" ./src # recurse a directory, with line numbersEvery flag below was checked against GNU grep’s man page on Debian 12.
Cheat sheet: the flags you actually use
| Task | Command | Flag |
|---|---|---|
| Case-insensitive search | grep -i error app.log | -i |
| Invert: lines that don't match | grep -v DEBUG app.log | -v |
| Search every file under a directory | grep -r "TODO" ./src | -r |
| Show the line number | grep -n "ERROR" app.log | -n |
| Count matching lines only | grep -c "ERROR" app.log | -c |
| Just the filenames that match | grep -l "TODO" *.js | -l |
| Match a whole word, not a substring | grep -w cat file.txt | -w |
| Match several patterns at once | grep -e warn -e error app.log | -e (repeatable) |
| Exclude a pattern while recursing | grep -r --exclude-dir=node_modules "foo" . | --exclude-dir |
| Lines of context around a match | grep -C 2 "panic" app.log | -C NUM |
| Only the matched text, not the whole line | grep -o 'error: .*' app.log | -o |
| Treat PATTERN as a literal string, not a regex | grep -F "a.b[c]" file.txt | -F |
| Use extended regex (no backslash before + ? | ( )) | grep -E 'foo|bar' file.txt | -E |
-i and -v: the two most common flags
-i ignores case in both the pattern and the input, so grep -i error matches Error, ERROR and error alike. -v inverts the match entirely — it prints every line that does not contain the pattern, which is the usual way to strip noise out of a log before reading it:
journalctl -u nginx | grep -vi "GET /health" # hide routine health checksRecursive search: -r, and narrowing it
grep -rn "function deploy" ./appPlain -r follows symlinks only when they are named directly on the command line, reads every file it finds, and silently skips binary files by default unless they look like they contain only text — a binary match prints binary file FILENAME matches instead of the line itself. Narrow a big tree before it gets slow or noisy:
grep -rn --include='*.ts' "TODO" ./src # only .ts files
grep -rn --exclude-dir={node_modules,.git} "TODO" . # skip whole directories
grep -rln "console.log" ./src # filenames only, for piping into another commandegrep -r and fgrep -r still work on Debian and Ubuntu for backward compatibility, but they are deprecated upstream in favour of grep -Er and grep -Fr.
Basic vs extended regex: -E, and why + and | need it
Plain grep uses Basic Regular Expressions (BRE) by default, where +, ?, | and parentheses are literal characters unless you escape them with a backslash. -E (or the egrep alias) switches to Extended Regular Expressions (ERE), where those characters work unescaped — the form most people expect from other tools:
| Meaning | Basic (default) | Extended (-E) |
|---|---|---|
| One or more | grep 'fo\+' | grep -E 'fo+' |
| Zero or one | grep 'colou\?r' | grep -E 'colou?r' |
| Alternation (OR) | grep 'cat\|dog' | grep -E 'cat|dog' |
| Grouping | grep '\(ab\)\+' | grep -E '(ab)+' |
-F is the opposite move: it turns off regex interpretation entirely and matches the pattern as a literal string, which is what you want for a pattern containing ., [, $ or other characters that mean something in a regex but nothing in your search term — a literal IP address or a file path, for instance.
Colour, context lines and line numbers
grep -n --color=always "error" app.log | less -Rgrep auto-colours matches when writing straight to a terminal and turns colour off automatically once the output is piped — --color=always overrides that, which is why it shows up paired with less -R (the flag that tells less to render colour codes instead of showing them as literal escape sequences). For context around a match rather than just the matching line itself:
grep -B 2 -A 5 "panic" app.log # 2 lines before, 5 lines after
grep -C 3 "panic" app.log # 3 lines of context on both sidesgrep in a pipeline
Three combinations come up constantly on a server:
ps aux | grep '[n]ginx' # the [n] trick excludes grep's own matching line
sudo ss -tlnp | grep ':443' # what is listening on port 443
history | grep rsync # find a command you ran beforeps aux | grep nginx on its own always matches one extra line — the grep nginx process itself, since ps sees it too. Writing the first character of the pattern as a single-character bracket expression, [n]ginx, matches the literal character n but no longer matches the literal string nginx that appears in grep’s own command line, so it drops out of the results. pgrep nginx avoids the whole problem by not matching itself in the first place; see finding and killing a process for that approach when the goal is a PID rather than just a count.
grep’s exit status, in scripts
grep returns 0 when it finds at least one match, 1 when it finds none, and 2 on an actual error (a missing file, bad syntax). That makes it useful directly inside a condition, with -q to suppress the output you don’t need:
if grep -q "^export PATH" ~/.bashrc; then
echo "already set"
else
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
fiWhen grep isn’t the right tool
- Structured logs (JSON, columns).
grepmatches text, not fields —jqfor JSON orawkfor columns will get you the actual field instead of a line that happens to contain the right string. - Watching a growing log live. Pair it with
tail -f:tail -f /var/log/nginx/error.log | grep --line-buffered error(--line-bufferedstops grep from waiting to fill an output buffer before printing, which otherwise delays matches on a live stream). - High CPU from a process you found with grep. See VPS high CPU usage for what to do once you’ve identified it, and checking disk usage if you were grepping logs because the disk is filling up.
Frequently asked
How do I make grep ignore case?
grep -i PATTERN FILE. It ignores case in both the pattern and the input, so -i error matches Error, ERROR and error alike.
How do I search every file in a directory?
grep -r PATTERN DIRECTORY recurses into every subdirectory. Narrow it with --include='*.ts' to limit by file type, or --exclude-dir=node_modules to skip whole directories.
What is the difference between grep and egrep?
egrep is the same as grep -E: it interprets the pattern as an extended regular expression, where +, ?, | and parentheses work unescaped. Plain grep uses basic regular expressions by default, where those characters need a backslash to mean the same thing. egrep is deprecated upstream in favour of writing grep -E directly.
How do I exclude a folder like node_modules from a recursive grep?
grep -r --exclude-dir=node_modules PATTERN . — repeatable as --exclude-dir={node_modules,.git} for more than one directory at once.