Guides

grep command: cheat sheet for -i, -v, -r and regex

grep PATTERN FILE prints matching lines. -i ignores case, -v inverts the match, -r recurses a directory. Flags, basic vs extended regex, and grep in a pipeline.

Published ~6 min read

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).

any Linux
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 numbers

Every flag below was checked against GNU grep’s man page on Debian 12.

Cheat sheet: the flags you actually use

TaskCommandFlag
Case-insensitive searchgrep -i error app.log-i
Invert: lines that don't matchgrep -v DEBUG app.log-v
Search every file under a directorygrep -r "TODO" ./src-r
Show the line numbergrep -n "ERROR" app.log-n
Count matching lines onlygrep -c "ERROR" app.log-c
Just the filenames that matchgrep -l "TODO" *.js-l
Match a whole word, not a substringgrep -w cat file.txt-w
Match several patterns at oncegrep -e warn -e error app.log-e (repeatable)
Exclude a pattern while recursinggrep -r --exclude-dir=node_modules "foo" .--exclude-dir
Lines of context around a matchgrep -C 2 "panic" app.log-C NUM
Only the matched text, not the whole linegrep -o 'error: .*' app.log-o
Treat PATTERN as a literal string, not a regexgrep -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:

any Linux
journalctl -u nginx | grep -vi "GET /health"   # hide routine health checks

Recursive search: -r, and narrowing it

any Linux
grep -rn "function deploy" ./app

Plain -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:

any Linux
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 command

egrep -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:

MeaningBasic (default)Extended (-E)
One or moregrep 'fo\+'grep -E 'fo+'
Zero or onegrep 'colou\?r'grep -E 'colou?r'
Alternation (OR)grep 'cat\|dog'grep -E 'cat|dog'
Groupinggrep '\(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

any Linux
grep -n --color=always "error" app.log | less -R

grep 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:

any Linux
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 sides

grep in a pipeline

Three combinations come up constantly on a server:

any Linux
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 before

ps 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:

bash
if grep -q "^export PATH" ~/.bashrc; then
  echo "already set"
else
  echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
fi
  • Structured logs (JSON, columns). grep matches text, not fields — jq for JSON or awk for 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-buffered stops 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.

Related articles

All guides