Searching File Contents with grep

grep is the command Linux users reach for constantly: it searches the contents of files (or any text stream) for lines that match a pattern, and prints those lines. The name comes from an old ed editor command, g/re/p — “globally search for a regular expression and print.” Whether you’re hunting for an error message in a log file, finding every place a function is called across a codebase, or filtering the output of another command, grep is usually the fastest tool for the job.

Overview: How grep Works

grep reads its input line by line — either from one or more files you name, or from standard input if no file is given — and tests each line against a pattern. If the line matches, grep writes that line to standard output (by default, the whole line, not just the matching part). If it doesn’t match, the line is discarded silently. grep never modifies the file it searches; it only reads and reports.

The “pattern” is a regular expression (regex) by default, not a literal string, which is why grep is so much more powerful than a plain text search. A regex is a mini-language for describing shapes of text: ^ anchors to the start of a line, $ anchors to the end, . matches any single character, * means “zero or more of the previous thing,” and character classes like [0-9] match a range. By default grep uses Basic Regular Expressions (BRE), where characters like (, ), |, and + are treated as literal characters unless escaped with a backslash. Passing -E switches to Extended Regular Expressions (ERE), where those characters have their special meaning without escaping — this is what most people expect, and what egrep (a deprecated alias for grep -E) used to provide.

Under the hood, grep is just another program that opens the files you name using the same open()/read() system calls any program uses, decodes them as a stream of bytes, and does a line-oriented scan. Nothing about it is magic or kernel-level — it’s why grep works identically on a regular file, a device file, or data piped in from another command: the kernel hands it a stream of bytes through a file descriptor, and grep doesn’t care where that stream originates. When you write ps aux | grep nginx, the kernel connects the standard output of ps to the standard input of grep through a pipe (an in-kernel buffer with a read end and a write end), and grep reads from file descriptor 0 exactly as if it were reading a file.

By default grep exits with status 0 if it found at least one match, and status 1 if it found none — this makes it useful directly inside if conditions in scripts, not just for printing matches to the terminal.

Syntax

grep [OPTIONS] PATTERN [FILE...]

If no FILE is given, grep reads from standard input, which is what makes it work at the end of a pipeline. The most commonly used options:

Option Meaning
-i Case-insensitive match (Error matches error, ERROR, etc.)
-v Invert match — print lines that do not match the pattern
-r, -R Recursively search all files under a directory (-R also follows symlinks)
-n Prefix each matching line with its line number in the file
-c Print only a count of matching lines, not the lines themselves
-l Print only the names of files that contain a match (not the lines)
-L Print only the names of files that do not contain a match
-w Match whole words only (the match must have word boundaries on both sides)
-x Match the entire line only, not just part of it
-E Use Extended Regular Expressions (lets you use (, |, + without backslashes)
-F Treat the pattern as a plain fixed string, not a regex (fastest, and safest for literal text)
-o Print only the matched portion of each line, not the whole line
-A N Print N lines of trailing context after each match
-B N Print N lines of leading context before each match
-C N Print N lines of context on both sides (shorthand for -A N -B N)
--color Highlight the matched text in the output (often on by default via an alias)

Examples

Example 1: A simple literal search

grep "error" /var/log/app.log

Output:

Aug  3 09:12:04 error: failed to connect to database
Aug  3 09:14:51 error: timeout while reading from socket
Aug  3 09:20:33 error: disk quota exceeded

This scans every line of /var/log/app.log and prints the ones containing the substring error anywhere in the line. Because no anchors or wildcards are used, grep treats error as a simple substring pattern here — it would also match a line containing terror or errorcode, since there’s no word boundary.

Example 2: Case-insensitive search with line numbers

grep -in "warning" /var/log/syslog

Output:

42:Aug  3 08:01:12 kernel: Warning: CPU temperature above threshold
187:Aug  3 08:45:33 systemd: Warning: unit myapp.service entered failed state

Combining -i and -n (short options can be stacked as -in) finds both Warning and warning regardless of case, and prefixes each hit with its line number — 42 and 187 — so you can jump straight to that line in an editor.

Example 3: Recursive search across a project

grep -rnw ~/projects/myapp -e "TODO"

Output:

/root/projects/myapp/src/auth.py:23:    # TODO: replace with proper token refresh logic
/root/projects/myapp/src/db.py:88:    # TODO: add connection pooling
/root/projects/myapp/README.md:15:TODO: document the deployment process

-r tells grep to walk into ~/projects/myapp and every subdirectory beneath it, searching each file it finds. -w restricts matches to the whole word TODO (so it wouldn’t match TODOLIST), and -e explicitly marks what follows as the pattern — useful when the pattern itself might start with a dash and could otherwise be mistaken for an option.

Example 4: Extended regex with alternation

grep -E "^(ERROR|FATAL|CRITICAL)" /var/log/app.log

Output:

ERROR: failed to connect to database
FATAL: worker process crashed
CRITICAL: disk usage at 98%

The ^ anchors the match to the start of the line, and (ERROR|FATAL|CRITICAL) means “one of these three alternatives.” Without -E, the parentheses and pipe would need backslashes (\(ERROR\|FATAL\|CRITICAL\)) to have this special meaning, since basic regex treats them as literal characters by default.

Example 5: Showing context around a match

grep -A 2 -B 2 "Segmentation fault" /var/log/app.log

Output:

Aug  3 10:02:01 app[1421]: starting worker thread
Aug  3 10:02:03 app[1421]: processing request id=88231
Aug  3 10:02:03 app[1421]: Segmentation fault (core dumped)
Aug  3 10:02:04 systemd: app.service: main process exited, code=killed
Aug  3 10:02:04 systemd: app.service: failed with result 'signal'

Instead of just the matching line, -B 2 prints the two lines before it and -A 2 the two lines after, giving you surrounding context — invaluable when a single log line about a crash means nothing without what led up to it.

How grep Works Step by Step

    For a command like grep -in "warning" /var/log/syslog, here’s what happens: (1) the shell parses the command line, recognizes -i and -n as combined options, and passes warning and /var/log/syslog as arguments to the grep program; (2) grep opens /var/log/syslog for reading; (3) it compiles the pattern warning into an internal regex matching structure, and because -i was given, it does this in a way that folds case before comparing; (4) it reads the file one line at a time, testing each line against the compiled pattern; (5) for every line that matches, it prints the line number (because of -n) followed by the line’s text to standard output; (6) once the file is exhausted, grep exits with status 0 if at least one line matched, or 1 if none did. When grep is used in a pipeline like ps aux | grep nginx, step (2) is replaced by reading from the pipe’s read end that the shell already connected to grep‘s standard input — everything else works the same way.

    Common Mistakes

    Mistake 1: Forgetting to quote a pattern with shell-special characters

    grep $5.00 prices.txt

    Here $5 looks like a literal dollar amount, but the shell sees $5 as a positional parameter (like $1) and expands it — usually to an empty string, since scripts rarely have a 5th argument — leaving grep to search for the leftover .00 as a regex (which matches any three characters). The fix is to single-quote the pattern so the shell passes it to grep untouched:

    grep '$5.00' prices.txt

    Single quotes prevent all shell expansion (variables, globs, command substitution), which is why they’re the safest default for grep patterns unless you specifically need the shell to expand something first.

    Mistake 2: Running grep on a directory without -r

    grep "TODO" ~/projects/myapp

    Output:

    grep: /root/projects/myapp: Is a directory

    grep expects files, not directories, unless you explicitly tell it to recurse. The fix is to add -r:

    grep -r "TODO" ~/projects/myapp

    Mistake 3: Expecting substring matches to respect word boundaries

    grep "cat" pets.txt

    Output:

    cat
    category
    concatenate

    Because grep matches patterns as substrings by default, searching for cat also matches lines containing category and concatenate — not just the word cat. If you only want whole-word matches, use -w:

    grep -w "cat" pets.txt

    This restricts matches to cat as a standalone word, bounded by non-word characters (spaces, punctuation, or line edges) on both sides.

    Best Practices

    • Quote your pattern in single quotes ('pattern') unless you deliberately want the shell to expand a variable first — this avoids surprises from $, *, and backticks.
    • Use -F (fixed string) when searching for a literal string with no regex meaning, such as a file path or IP address — it’s faster and avoids accidentally-special characters like . being treated as “any character.”
    • Use -r (or the newer grep -R variant, or rg/ripgrep if installed) when searching a whole project tree instead of looping over files yourself.
    • Prefer -E over escaping every parenthesis and pipe in a basic regex — extended regex is far more readable for alternation and grouping.
    • Use -w or -x when you need exact word or line matches, especially when filtering things like process names or config keys where substring matches cause false positives.
    • Combine -l with -r (grep -rl "pattern" .) when you only need a list of matching files, not every matching line — it’s much faster on large trees since grep can stop reading a file after the first hit.
    • Check grep‘s exit status ($?) in scripts to branch on whether a pattern was found, rather than parsing its printed output.

    Practice Exercises

    • You have a log file at /var/log/app.log with entries at many severity levels. Write a single grep command that prints only the lines starting with ERROR or WARN, using extended regex and an anchor.
    • You have a directory ~/projects/website containing HTML and CSS files. Write a command that recursively finds every file containing the exact word deprecated (not deprecation or undeprecated) and lists only the file names, not the matching lines.
    • You want to know how many lines in /etc/passwd do not start with a # comment character. Write a grep command that inverts the match on lines beginning with # and counts the remaining lines.

    Summary

    • grep searches text line by line and prints lines matching a pattern; it reads from files or from standard input in a pipeline.
    • Patterns are regular expressions by default (Basic Regular Expressions); use -E for Extended Regular Expressions or -F for a plain literal string.
    • Common flags: -i ignores case, -v inverts the match, -r recurses into directories, -n shows line numbers, -c counts matches, -w matches whole words only.
    • Always single-quote patterns to stop the shell from expanding $, *, or backticks before grep ever sees them.
    • grep exits 0 when it finds a match and 1 when it doesn’t, which makes it directly usable inside script conditionals.