grep across many files
Real projects contain hundreds of files. Locating every use of a function, or every file that still says TODO, cannot be done one file at a time, and this is a search that working engineers run many times a day. Every editor's project-wide search box is a version of it. grep has flags built for exactly this:
| Command | What it does |
|---|---|
grep -r pattern directory | Searches recursively through the directory and all its subdirectories. Each hit prints as path:matching line, so the source file is always visible. |
grep -rn pattern . | Adds line numbers, making each result path:line:text, the exact address of the match. |
grep -rl pattern . | Prints only the list of file names containing a match, one per line, for when the question is which files rather than which lines. |
One grep, two files, no directory hunting
This example builds a tiny project with a TODO note in two different files, then a single grep -rn finds both without being told where to look. The | sort keeps the order predictable, since grep visits files in whatever order the filesystem hands them over.
mkdir -p app/src app/docs echo "# TODO: write the intro" > app/docs/readme.md printf 'print("start")\n# TODO: handle errors\nprint("end")\n' > app/src/main.py echo "just notes" > app/notes.txt grep -rn "TODO" app | sort
Output
app/docs/readme.md:1:# TODO: write the intro app/src/main.py:2:# TODO: handle errors
Each result carries three fields separated by colons: the path, the line number, and the matching text. Swapping -rn for -rl collapses the same search down to just the two file names, which is the form to reach for when the file list is the answer.
Patterns, not just words
The pattern handed to grep is not plain text. It is a regular expression, regex for short: a small language for describing the shape of text. Everything so far has used the boring subset, where letters simply match themselves. Four symbols unlock the useful subset:
| symbol | matches |
|---|---|
^ | the start of the line |
$ | the end of the line |
. | any single character |
[abc] | one character from the set |
So ^error matches lines that begin with error rather than lines that merely mention it, and full$ matches lines that end with full. Quote the pattern, as in grep '^error' file, so the shell passes it to grep untouched. That is the same precaution the find pattern needed in lesson 4-2.
Regular expressions go much deeper and deserve a course of their own, but ^ and $ alone sharpen a large share of real searches.
Anchoring a search to the start or end of a line
A plain grep error would match three of these four lines. Anchoring with ^ keeps only the lines that start with error, and full$ finds the single line that ends with full.
printf 'error: disk full\nuser error detected\nerror at line 9\nno problems here\n' > app.log grep '^error' app.log grep 'full$' app.log
Output
error: disk full
error at line 9
error: disk fullThe three output lines come from two commands. The first grep printed two lines, skipping user error detected because that line only mentions error in the middle. The second printed one, and it happens to be a line the first command also matched, since error: disk full both begins with error and ends with full.
In a grep pattern, ^ anchors the match to the start of the line. So ^user matches only lines that begin with user, and ignores lines where user appears later in the text.
$ is its mirror image at the end of the line. Because ^ carries this special meaning, matching a literal caret character requires escaping it as \^.
The two anchors combine naturally with the counting flag from lesson 4-1. Here a five-line log is written, grep '^user' lists the lines that start with user, and grep -c 'ok$' reports how many lines end with ok instead of printing them.
printf 'user alice logged in\nsystem rebooted\nuser bob logged in\nbackup ok\nrestore ok\n' > events.log grep '^user' events.log grep -c 'ok$' events.log
Output
user alice logged in user bob logged in 2
Both patterns stay in single quotes so the shell hands ^user and ok$ to grep exactly as written. The system rebooted line is excluded from the first result because the anchor demands user at position one, and the trailing 2 counts backup ok and restore ok.