Searching Inside Files: grep & Regular Expressions
- Use grep to print the lines that match a pattern, and reach for -i, -n, -r, -v, -c, -l when you need them
- Explain why grep's pattern is a regular expression, not a shell glob — and why . and * mean different things than you expect
- Build patterns from the regex toolkit: anchors (^ $), character classes ([...]), the any-char dot, and quantifiers (* + ? with -E)
- Combine grep with pipes and find to filter command output and search a whole tree of files
grep prints the lines that match
Learn grep's basic shape and the everyday flags — and see how it differs from find.
You've seen how
findlocates files by their attributes. `grep` answers the other half of the question: it looks inside files and prints the lines that match a pattern. Wherefindhands you filenames,grephands you the matching text.The basic shape is `grep PATTERN [file…]`. grep reads the file line by line and prints every line that contains the pattern:
- Give it a file and it searches that file.
- Give it no file (or a
-) and it reads from standard input — which is what makessome-command | grep PATTERNthe everyday way to filter another command's output.
A handful of flags cover almost everything you'll do:
- `-i` — ignore case (
erroralso matchesError,ERROR). - `-n` — prefix each hit with its line number.
- `-r` — search recursively through a whole directory tree (like
find+grepin one). - `-v` — invert: print the lines that don't match.
- `-c` — print a count of matching lines instead of the lines.
- `-l` — print just the filenames that contain a match, not the lines.
Example — not runnable on your lab box
grep -n 'error' /var/log/app.log42:error: failed to open socket 87:error: retry limit reached
Print every line of app.log containing 'error', each with its line number (-n). grep read the whole file and kept only the two matching lines — the fastest way to pull the signal out of a noisy log. ps aux | grep -v grep | grep nginxwww-data 8123 0.0 0.4 nginx: worker process
Filter another command's output through a pipe: take the process list, drop grep's own line (-v grep), then keep only nginx rows. With no filename, grep reads standard input — that's why it pairs so naturally with |. The pattern is a regex, not a glob
Grasp the single most important idea: grep's pattern is a regular expression, where the metacharacters mean different things than shell globs.
Here is the idea that trips up everyone coming from shell globbing. In the Quoting & Globbing lesson you learned the shell's
*means "any run of characters" in a filename. *grep's pattern is not a glob — it's a regular expression (regex)*, and the same symbols mean something different:- `.` matches any single character (in a glob,
.is just a literal dot). - *`
** means **"zero or more of the thing before it"** — it modifies the *previous* character, it does not stand alone for "anything". Soa` means "zero or more a's", and `.` (dot-star) is the regex way to say "any run of characters".
That one difference is the whole gotcha. In a glob,
a*matches names starting witha. In a regex,a*matches zero or moreas — which every line technically has, sogrep 'a*'matches everything. To mean "any characters" in grep you write *`.**, not*`.And just like with globs, you quote the pattern —
grep 'a.*b' file— so the shell doesn't try to expand any*or other special characters before grep ever sees them. Quoting hands the pattern to grep untouched; grep then interprets it as a regex.printf 'cat\ncot\ncart\ndog\n' | grep 'c.t'cat cot
The regex c.t means 'c, then any single character, then t'. It matches 'cat' and 'cot' but NOT 'cart' (two characters between c and t) or 'dog'. The dot is exactly one character — that's the regex . in action. Check yourself
In a grep pattern, what does the dot ( . ) match?
- `.` matches any single character (in a glob,
The regex toolkit: anchors, classes, quantifiers
Build real patterns from the handful of pieces that cover most searches.
Most useful patterns come from combining a small set of pieces:
- Anchors tie a match to a position. `^` means "start of line", `$` means "end of line". So
^errormatches lines that begin witherror, anddone$matches lines that end withdone.^$matches an empty line. - Character classes match one character from a set. `[abc]` matches an
a,b, orc; `[0-9]` any digit; `[a-z]` any lowercase letter. Put `^` first inside the brackets to negate: `[^0-9]` matches any character that is not a digit. - The dot `.` matches any single character (from the last step).
- Quantifiers say how many. *`
** means "zero or more of the previous". With **-E** (extended regex) you also get **+** (one or more), **?** (zero or one), **{2,4}** (between 2 and 4), **|** (either/or), and **( )`** to group. - Word boundaries `\b` match the empty position at the edge of a word — where a word character (a letter, digit, or
_) meets a non-word character or the start or end of the line.\bthe\bmatches the wordtheon its own but not thetheinsidethere,other, ortheme— the way to match a whole word without pinning to the whole line as^…$does.
A note on
-E: basic grep supports^ $ . [] *. The handier operators+ ? | ( ) {}require `grep -E` (or the older nameegrep). When a pattern isn't matching what you expect, a missing-Eis a common reason.printf 'GET /a 200\nPOST /b 404\nGET /c 500\n' | grep -E '(404|500)$'POST /b 404 GET /c 500
The extended pattern (404|500)$ means 'either 404 or 500, at the end of the line'. The | alternation and the ( ) grouping both need -E; the $ anchors the match to the line's end, so a 404 appearing mid-line wouldn't count. printf 'id: 7\nid: 42\nid: x\n' | grep -E 'id: [0-9]+'id: 7 id: 42
[0-9]+ means 'one or more digits' (the + needs -E). It matches the numeric ids and rejects 'id: x', because x is not a digit. Character class plus quantifier is the everyday way to match 'a number'. - Anchors tie a match to a position. `^` means "start of line", `$` means "end of line". So
grep with pipes and find: searching at scale
Filter command output and search whole trees by combining grep with | and find.
grep earns its keep when you point it at many lines or many files:
- Filter a command's output —
command | grep PATTERN. Because grep reads standard input, it drops into any pipeline to keep only the interesting lines. Stack them:command | grep X | grep -v Y. - Search a whole tree — `grep -r PATTERN dir` walks
dirand every subdirectory, searching all files. Add-nfor line numbers and-lto list just the files. This is the "where in my project is this string?" command. - Pair with find when you want to search only certain files:
find . -name '*.conf' -print0 | xargs -0 grep -l PATTERNfinds config files, then greps inside them for the ones containing the pattern (the-print0 | xargs -0pair keeps it safe when names have spaces, straight from the Finding Files lesson). - `-o` prints only the matched text, not the whole line — handy for pulling values out:
grep -oE '[0-9]+'prints just the numbers it finds.
Between
grep -rfor "search everything here" andfind … | xargs grepfor "search a chosen set", you can locate any string across a codebase or a log directory in one line.Example — not runnable on your lab box
grep -rn 'TODO' src/src/parser.c:88: // TODO: handle empty input src/main.c:12: // TODO: parse flags
Recurse through src/ (-r), show line numbers (-n), and print every line containing TODO along with its file and line. This one command is how you find every loose end in a whole project at once. find . -name '*.log' -print0 | xargs -0 grep -l 'timeout'Find the .log files, then hand them to grep -l to print only those that CONTAIN 'timeout'. find chooses which files; grep searches inside them. The -print0/-0 pair is the space-safe hand-off. Your terminal supplies the matching filenames. Check yourself
You want to search every file under the current directory for the word 'password' and see line numbers. Which single grep command does it?
- Filter a command's output —
Now do it — search text with flags and a regex
Put it together: make a file, then pull lines out of it by flag and by pattern.
One continuous experiment. You'll build a small log, then search it by plain text, by flag, and by regular expression — the everyday grep workflow end to end.
printf 'INFO start\nERROR disk full\ninfo retry\nERROR timeout\nWARN slow\n' > demo.log && grep 'ERROR' demo.logERROR disk full ERROR timeout
Create a five-line log, then print the lines containing ERROR. Plain grep is case-sensitive, so the lowercase 'info' line and the WARN line are left out — only the exact-case ERROR lines match. grep -in 'info' demo.log; echo '--- count of ERROR lines ---'; grep -c 'ERROR' demo.log1:INFO start 3:info retry --- count of ERROR lines --- 2
grep -in 'info' matches BOTH 'INFO' and 'info' (-i ignores case) and shows line numbers (-n). Then grep -c 'ERROR' reports 2 — the number of matching lines, not the lines themselves. grep -E '^(ERROR|WARN)' demo.log; echo '--- lines NOT starting with a capital ---'; grep -v '^[A-Z]' demo.logERROR disk full ERROR timeout WARN slow --- lines NOT starting with a capital --- info retry
^(ERROR|WARN) anchors the match to the start of the line and accepts either word (alternation needs -E). Then -v '^[A-Z]' inverts the match to show lines that do NOT begin with a capital letter — only the lowercase 'info retry' line qualifies. grep -oE '[a-z]+$' demo.log; echo '--- cleaning up ---'; rm demo.logstart full retry timeout slow --- cleaning up ---
-oE '[a-z]+$' prints ONLY the matched text (-o): a run of one-or-more lowercase letters anchored to the end of the line. You've now searched by text, case-insensitively, by count, by anchored alternation, by inversion, and pulled out just the match — the whole grep toolkit in one sitting.
- grep PATTERN [file] prints the lines that match; with no file it reads standard input, which is why command | grep PATTERN filters output. Key flags: -i (ignore case), -n (line numbers), -r (recurse), -v (invert), -c (count), -l (list filenames).
- grep's pattern is a regular expression, not a shell glob. Regex . = any single character; regex = zero-or-more of the PREVIOUS character. To mean 'any run of characters' write . — quote the pattern so the shell doesn't touch it.
- Build patterns from anchors (^ start, $ end), character classes ([0-9], [a-z], [^…] to negate), the dot, and quantifiers (* everywhere; + ? | ( ) {} with -E / egrep). Anchors are how you stop a pattern from over-matching.
- Search at scale by combining grep with pipes (cmd | grep) and with find (find … -print0 | xargs -0 grep); grep -rn PATTERN . searches a whole tree, and grep -o prints just the matched text.
| You write | Effect |
|---|---|
| grep 'error' file | print lines of file containing error |
| cmd | grep -v skip | keep the output lines that DON'T match skip |
| grep -rn 'TODO' . | recurse the tree, print matches with line numbers |
| grep -i 'warn' file | match warn/WARN/Warn (case-insensitive) |
| grep '^error' / 'done$' | lines starting with error / ending with done |
| grep -E '[0-9]+' | one or more digits (+ needs -E) |
| grep -oE '[a-z]+' | print only the matched runs of lowercase letters |
