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 newergrep -Rvariant, orrg/ripgrep if installed) when searching a whole project tree instead of looping over files yourself. - Prefer
-Eover escaping every parenthesis and pipe in a basic regex — extended regex is far more readable for alternation and grouping. - Use
-wor-xwhen you need exact word or line matches, especially when filtering things like process names or config keys where substring matches cause false positives. - Combine
-lwith-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.logwith entries at many severity levels. Write a singlegrepcommand that prints only the lines starting withERRORorWARN, using extended regex and an anchor. - You have a directory
~/projects/websitecontaining HTML and CSS files. Write a command that recursively finds every file containing the exact worddeprecated(notdeprecationorundeprecated) and lists only the file names, not the matching lines. - You want to know how many lines in
/etc/passwddo not start with a#comment character. Write agrepcommand that inverts the match on lines beginning with#and counts the remaining lines.
Summary
grepsearches 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
-Efor Extended Regular Expressions or-Ffor a plain literal string. - Common flags:
-iignores case,-vinverts the match,-rrecurses into directories,-nshows line numbers,-ccounts matches,-wmatches whole words only. - Always single-quote patterns to stop the shell from expanding
$,*, or backticks beforegrepever sees them. grepexits0when it finds a match and1when it doesn’t, which makes it directly usable inside script conditionals.
