When something breaks, you usually need one answer fast: where did that string appear? That is what grep is for. It reads a file (or many files), keeps the lines that match your pattern, and drops the rest.

You do not need to memorize every flag on day one. Start with a plain search, then layer on options as the problem gets noisier. The sections below walk through the commands you will reach for most often — with enough context that each one feels intentional, not magical.

Warm-up: search one file

Before diving into flags, give yourself a small playground. The short log below mirrors what you see in real services: timestamps, levels, and a couple of failures mixed with healthy traffic.

Create it once, then reuse it for every example in this article:

cat > app.log <<'EOF'
2026-08-04 09:01:12 INFO  server started on :8080
2026-08-04 09:02:44 WARN  retrying connection to db
2026-08-04 09:03:01 ERROR failed to connect: timeout
2026-08-04 09:03:15 INFO  request GET /health 200
2026-08-04 09:04:02 ERROR failed to connect: refused
2026-08-04 09:05:18 INFO  request POST /login 401
EOF

With the file ready, ask for every line that contains ERROR. This is the simplest useful form of grep: pattern first, filename second.

grep ERROR app.log

You should see only the two failure lines:

2026-08-04 09:03:01 ERROR failed to connect: timeout
2026-08-04 09:04:02 ERROR failed to connect: refused

Note: If nothing prints, that usually means there was no match — not that the command failed. grep stays quiet when it finds nothing, which can feel surprising the first time.

Logs and config files are rarely consistent about casing. When you are unsure whether the text is Error, ERROR, or error, add -i and let grep ignore case for you:

grep -i error app.log

Same matches as before, without you having to guess the exact spelling. Reach for -i early when searching human-written notes, commit messages, or mixed-case application logs.

See context around matches

A lone error line rarely tells the full story. You usually want to know what happened just before the failure — a warning, a retry, a bad request. Context flags pull neighboring lines into the result so you can read the incident in place.

To include lines that appear before each match, use -B (before). The number is how many surrounding lines you want:

grep -B 2 ERROR app.log

To include lines that appear after each match, use -A (after):

grep -A 2 ERROR app.log

When you want a small window on both sides, -C (context) is the convenient middle ground:

grep -C 1 ERROR app.log

Note: While you are hunting a spike in production, start with -C 3 or -C 5. Too little context and you keep re-running the search; too much and you are reading the whole file again. Adjust the number until the story around each match is clear.

Line numbers and match counts

Once you know a match exists, the next question is often where or how many. Line numbers help you jump straight into an editor. Counts help you decide whether a problem is rare or systemic.

Add -n to prefix every matching line with its line number in the file:

grep -n ERROR app.log

Typical output looks like this — the number before the colon is the line you can open directly:

3:2026-08-04 09:03:01 ERROR failed to connect: timeout
5:2026-08-04 09:04:02 ERROR failed to connect: refused

When you only care about volume, not the text itself, -c reports how many lines matched:

grep -c ERROR app.log

For the sample log, that prints 2.

Note: With a single file, -c returns one number. Point it at several files (for example *.log) and you get a count per file, which is handy when you are comparing environments or rotating log shards:

grep -c ERROR *.log

Invert the match

Sometimes the useful view is everything except a pattern. Inverting the match is a clean way to strip noise without rewriting the file.

Use -v to keep lines that do not contain the pattern. Against our sample, this hides the errors and leaves the healthy and warning traffic:

grep -v ERROR app.log

You can also stack filters. First keep INFO lines, then drop the health-check chatter that clutters most dashboards:

grep INFO app.log | grep -v health

Note: Piping one grep into another is normal and readable. Prefer a short pipeline over one giant regex until the filter logic becomes stable enough to consolidate.

Search a whole tree

Real work rarely lives in one file. From a project root, recursive search walks the directory tree and reports every matching line with its path.

The -R flag (recursive) is the usual starting point. Quoting the pattern keeps the shell from expanding special characters:

grep -R "TODO" .

That works, but it can drown you in node_modules, build folders, and .git history. Narrow the search with --include for file types you care about and --exclude-dir for folders you never want to touch:

grep -R --include='*.{ts,tsx,js,jsx}' --exclude-dir={node_modules,dist,.git} "useEffect" .

The same idea works for documentation. To scan only markdown under a docs/ folder:

grep -R --include='*.md' "frontmatter" ./docs

Note: Prefer a recursive grep over hand-rolled find … -exec loops unless you need unusual path logic — size, age, or custom prune rules. For those cases, see Linux find. Keeping a text search in one grep command makes it easier to tweak filters and share with teammates.

When ripgrep is a better daily driver

On a minimal server or inside a portable shell script, stick with grep — it is everywhere. On your laptop, ripgrep (rg) often feels like grep with sensible defaults for source trees: it skips ignored files, respects .gitignore, and stays fast on large repos.

Two everyday examples:

rg "useEffect" -t ts
rg "TODO" -g '!dist'

Note: Think of rg as a productivity upgrade for code search, not a replacement you must learn before using grep. Everything you practice here still transfers.

Useful regex patterns (practical set)

Plain text search covers a lot. When the shape of the text matters — status codes, emails, addresses — a small amount of regex goes a long way. By default grep uses basic regular expressions. Add -E for extended regex so you write fewer awkward backslashes.

Here is a pattern that catches HTTP-style 4xx and 5xx codes at the end of a line. It is perfect for access logs where the status sits near the end of each entry:

grep -E ' (4[0-9]{2}|5[0-9]{2})$' app.log

When you are cleaning a contact dump, a practical email-shaped pattern finds most real addresses without pretending to be RFC-perfect:

grep -E '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' contacts.txt

The same mindset works for IP-looking strings during triage. Good enough for investigation; tighten later if you need validation:

grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log

Anchors help when position matters. A caret (^) means “starts with,” and a dollar ($) means “ends with”:

grep -E '^ERROR' messages.txt
grep -E '\.log$' file-list.txt

Note: Special characters in your search text — brackets, dots, pipes — can be interpreted as regex. When you want a literal string, skip the guessing and use -F (fixed string). grep will treat the pattern as plain text:

grep -F 'failed to connect: timeout' app.log
grep -F 'array[0]' src/main.c

If a search suddenly returns nothing and you know the text is there, try -F before rewriting the pattern.

Word boundaries and whole lines

Partial matches create false confidence. Searching for port can happily match support or portable. When you care about a whole word, say so explicitly.

The -w flag requires word boundaries around the pattern:

grep -w port config.env

When the entire line must equal the pattern — nothing more, nothing less — use -x:

grep -x 'ERROR' status.txt

Note: Reach for -w whenever the needle is a short token that might appear inside longer identifiers. It is one of the simplest ways to cut false positives without inventing a clever regex.

Multiple patterns

Incidents rarely involve a single keyword. You often want errors and warnings, or a short list of known failure phrases, in one pass.

Extended regex lets you combine alternatives with | (OR). This keeps WARN and ERROR lines together so you can scan severity in one view:

grep -E 'ERROR|WARN' app.log

When the list grows — or you reuse the same set of needles across files — put one pattern per line in a file and point grep at it with -f:

cat > patterns.txt <<'EOF'
timeout
refused
unauthorized
EOF
grep -f patterns.txt app.log

Note: A patterns file is easier to review and version than a long one-liner. It also keeps your shell history cleaner when the list changes week to week.

Pipe-friendly workflows

grep is at its best as one stage in a pipeline. You generate text with another command, filter it, then hand the result to sort, awk, or a script. The examples below are patterns you will reuse across machines.

To list running processes related to Node, pipe ps into grep. The unusual [n]ode pattern matches node while avoiding a match on the grep process itself:

ps aux | grep -i '[n]ode'

On a server, failed SSH attempts usually leave clear breadcrumbs. Distro paths vary (auth.log, secure, and so on), but the filter idea stays the same:

sudo grep -E 'Failed password|Invalid user' /var/log/auth.log

When you need unique client IPs behind HTTP 500 responses, combine grep with awk and sort:

grep ' 500 ' access.log | awk '{print $1}' | sort -u

Sometimes you only need filenames, not the matching lines. -l lists files that contain the pattern; -L lists files that do not:

grep -Rl "API_KEY" .
grep -RL "license" ./src

Note: Filename-only mode is ideal for audits — “which files still mention this secret?” or “which sources lack a license header?” — without flooding the terminal with every hit.

Quiet checks in scripts

In automation, the interesting part is often the exit status, not the printed lines. You want a script to branch: if the log contains errors, fail the job; otherwise continue.

-q (quiet) suppresses output and lets you use grep inside an if. A match yields exit code 0; no match yields 1:

if grep -q ERROR app.log; then
  echo "build log has errors"
  exit 1
fi

Note: This is the idiomatic way to ask a yes/no question with grep. Prefer -q over capturing output and testing whether a string is empty — the intent stays obvious to the next person reading the script.

Quick reference card

Keep this nearby until the flags become muscle memory:

GoalCommand
Case-insensitivegrep -i pattern file
Line numbersgrep -n pattern file
Count matchesgrep -c pattern file
Invertgrep -v pattern file
Contextgrep -C 2 pattern file
Recursivegrep -R pattern dir
Fixed stringgrep -F 'lit[eral]' file
Extended regexgrep -E 'a|b' file
Whole wordgrep -w word file
Quiet (scripts)grep -q pattern file
Filenames onlygrep -l pattern *
Patterns filegrep -f patterns.txt file

Practice drills

Use the sample app.log (or your own project) and try these without peeking. The point is to choose the flag with intent, not to memorize syntax under pressure.

  1. Print only WARN and ERROR lines, with two lines of context around each match.
  2. Count how many lines contain the word request.
  3. List every .ts file under src/ that mentions TODO, while skipping node_modules.
  4. Extract the unique error messages that appear after the ERROR token.
  5. Write a one-liner that exits non-zero when app.log contains ERROR.

When you are ready to compare, here are solid answers — not the only ones, but clear and portable:

grep -E 'WARN|ERROR' -C 2 app.log
grep -c request app.log
grep -R --include='*.ts' --exclude-dir=node_modules 'TODO' src
grep ERROR app.log | sed 's/.*ERROR //' | sort -u
grep -q ERROR app.log; echo $?

If you can work through those five comfortably, you already cover most real grep work: log triage, codebase search, and shell pipelines. Start simple, then tighten with -w, -F, --include, and context only when the first pass gets noisy.