Searching Inside Files
cat and less show you a file’s contents, but neither one can answer a question as basic as “does this file mention the word ’error’ anywhere?” without you reading every line yourself. grep is the tool for that — and it’s one you’ll reach for constantly, whether you’re hunting through a single file or piping a command’s output straight into it.
The Basic Shape
grep searches for a pattern inside a file (or files) and prints every line that matches.
grep "error" logfile.txtThis prints every line in logfile.txt containing the text error, exactly as it appears. Nothing fancy yet — but even this basic form is doing more than it looks like, because “error” isn’t just a literal string to grep. It’s a pattern, and patterns are where grep earns a whole file to itself.
Matching is Case-Sensitive By Default
grep "error" logfile.txt will not match a line containing Error or ERROR — only the exact case you typed. Add -i to ignore case entirely:
grep -i "error" logfile.txtThis is worth reaching for by default when you’re not certain how something was capitalized in the file you’re searching, which in practice is most of the time.
Useful Flags Beyond Matching Text
A handful of flags come up constantly enough to know before we even get to patterns:
| Flag | Effect |
|---|---|
-i | Case-insensitive matching |
-v | Invert the match — show lines that don’t match |
-n | Show line numbers alongside each match |
-r | Search recursively through a directory |
-c | Print a count of matching lines instead of the lines themselves |
-l | Print only the names of files with at least one match, not the matches themselves |
-v in particular is easy to forget exists and surprisingly useful — it’s how you answer “show me every line that isn’t a comment” or “show me every process that isn’t the one I’m looking for,” rather than only ever searching for what you want to find.
grep -v "^#" config.txtThat example uses a ^ — which brings us to the actual reason grep gets its own chapter.
Patterns, Not Just Text: An Introduction To Regular Expressions
By default, grep treats your search term as a regular expression (often shortened to “regex”) — a mini pattern language, not just literal text. Most of the time a plain word behaves exactly like you’d expect, which is why the examples above worked without needing any explanation. But a handful of characters mean something special, and knowing even a handful of them turns grep from “find this exact word” into “find anything shaped like this.”
| Symbol | Meaning | Example | Matches |
|---|---|---|---|
^ | Start of line | ^error | Lines starting with “error” |
$ | End of line | error$ | Lines ending with “error” |
. | Any single character | err.r | “error”, “errxr”, “err5r” |
* | Zero or more of the previous character | err*or | “eor”, “error”, “errrror” |
[...] | Any one character from this set | [Ee]rror | “Error” or “error” |
A few of these deserve a closer look, because they’re not as intuitive as they first seem.
^ and $ anchor a match to a position, not a character. ^error doesn’t mean “error near the start” — it means the very first characters on the line must be exactly error. This is why the -v "^#" example above works as a way to skip comment lines: it excludes any line where # is the first character, without caring what comes after it.
. matches literally any character — including a period. This trips people up constantly when searching for something like an actual file extension or IP address. grep "192.168.1.1" will also match 192x168y1z1, because unescaped, . means “any character,” not “a literal dot.” To match a literal period, escape it with a backslash: 192\.168\.1\.1.
* applies to the character immediately before it, not to the whole pattern. err*or means “err, then zero or more of the letter r, then or” — matching “eor” (zero extra r’s) just as validly as “error” (one extra r) or “errror” (two extra r’s). This is the single most common regex misunderstanding: people expect * to mean “anything,” but on its own it only repeats whatever came immediately before it.
Tip
If you want “match anything, any length,” that’s actually the combination .* — “any character” followed by “zero or more of it.” grep "start.*end" matches any line containing “start” followed eventually by “end,” with anything at all in between. This combination comes up so often it’s worth memorizing as a pair.
grep and Extended Regular Expressions
If you are using grep to match Extended Regular Expressions (just some more character set with special meaning), you should specify -E option to say Hey! go beyond basic regular expression, search with extended regular expression.
| ERE | Meaning | Example | Matches |
|---|---|---|---|
() | Grouping | (ab)+ | ab, abab, ababab |
{} | Repetition count | a{3} | aaa |
? | Zero or one occurrence | colou?r | color, colour |
+ | One or more occurrences | a+ | a, aa, aaa |
Example:
grep colou?r fileMatches literal ? but the following:
grep -E colou?r fileAs -E is specified, now ? has different meaning i.e. Match Zero or one occurrence of preceding character or group and in this case the preceding charater is u.
Quoting Your Pattern
You’ve probably noticed every example wraps the pattern in double quotes. This isn’t just style — it matters. Left unquoted, the shell itself tries to interpret characters like * and $ before grep ever sees them, which can silently change what you’re actually searching for depending on what files happen to be in your current directory. Quoting the pattern guarantees grep receives exactly what you typed.
Searching Multiple Files And Whole Directories
Give grep more than one file and it searches all of them, prefixing each match with the filename it came from:
grep "error" logfile1.txt logfile2.txtTo search every file in a directory, recursively, add -r:
grep -r "TODO" projects/This walks into every subdirectory of projects/, checking every file it finds, and reports the filename and matching line for each hit — one of the most common ways grep actually gets used day-to-day, especially combined with -n to jump straight to the right line once you know which file to open.
Piping Into grep
Remember pipes from the previous chapter? This is where they become genuinely essential. Instead of searching a file, you can search any command’s output by piping it into grep:
ls -la /etc | grep "conf"This lists /etc, then filters that listing down to only lines containing “conf” — without grep ever needing to know or care that its input came from ls instead of a file. You’ll use this pattern relentlessly once you’re inspecting running processes or reading logs later in this course: run a command that produces a lot of output, then pipe it into grep to narrow it down to the one thing you actually care about.
flowchart LR
A["ls -la /etc"] -->|"full listing"| B["grep 'conf'"]
B --> C["Only lines containing 'conf'"]
What’s Next
grep finds lines inside files that match a pattern. But it can’t replace what it finds — you need another tool. Also it has no idea how to find files themselves scattered across a directory tree based on their name, size, or age — that’s a different problem, with a different tool. So in next few chapters, we will be talking about those.