Zsh Shell Tutorial #17: Master grep for Text Searching
Video: Zsh Shell Tutorial #17: Master grep for Text Searching by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 17: Master grep — Search Files for Patterns
grep pattern file.-icase-insensitive,-nline numbers,-ccount,-vinvert,-rrecursive.-Eextended regex.^start of line,$end.-A 2 -B 2for context. The single most useful Unix command for finding things.
grep searches text for patterns. You'll use it dozens of times a day.
Basic search
cat > colors.txt <<EOF
Red Apple
green grape
Blue Berry
red cherry
ORANGE peel
blueberry jam
EOF
grep "red" colors.txt
# red cherry
grep pattern file prints lines containing pattern. Default is case-sensitive.
Case-insensitive: -i
grep -i "red" colors.txt
# Red Apple
# red cherry
-i ignores case for both pattern and text.
Line numbers: -n
grep -n "blue" colors.txt
# 6:blueberry jam
-n prefixes each match with its line number.
Count: -c
grep -ic "blue" colors.txt
# 2
-c prints just the count of matching lines (not the matches themselves).
Invert: -v
grep -vi "red" colors.txt
# green grape
# Blue Berry
# ORANGE peel
# blueberry jam
-v prints lines that DON'T match. Useful for filtering out noise:
ps aux | grep -v grep # find processes, exclude the grep itself
Multiple files
grep "TODO" *.py
# main.py: # TODO: refactor
# utils.py: # TODO: add tests
Match multiple files. Output prefixes each line with the filename (when more than one file).
Recursive: -r (or -R)
grep -r "TODO" project/
# project/src/app.py:# TODO fix bug
# project/src/main.js:# TODO add tests
# project/docs/readme.md:TODO update docs
-r walks directories. Use it for "where in this project is X?"
To restrict to specific file types:
grep -r --include="*.py" "TODO" project/
grep -r --include="*.{js,ts}" "import" src/
# Or exclude:
grep -r --exclude-dir=node_modules "TODO" .
grep -r --exclude="*.min.js" "console.log" .
Show context: -A, -B, -C
grep -B 1 -A 2 "ERROR" log.txt # 1 before, 2 after
grep -C 3 "ERROR" log.txt # 3 lines on each side
Context flags help when matches are part of a larger block.
Multiple patterns
grep -E "(error|warning|fail)" log.txt
grep -e "error" -e "warning" log.txt
-E enables extended regex (ERE) — alternation |, grouping (), quantifiers +, ?. Without -E, those need backslash escape.
-e pattern lets you specify multiple patterns separately.
For literal strings (no regex), use -F:
grep -F "192.168.1.1" log.txt # treat dots as literal
-F is faster too — no regex engine.
From a list of patterns
grep -f patterns.txt logfile.txt
-f file reads patterns from file, one per line. Useful for matching against many keywords.
Just the matching part: -o
echo "Email: alice@example.com" | grep -oE "[a-z]+@[a-z]+\.[a-z]+"
# alice@example.com
-o prints only the matched portion of each line, not the whole line. Combine with regex to extract.
Regex basics
^pattern # start of line
pattern$ # end of line
. # any single character
[abc] # one of a, b, c
[^abc] # NOT one of a, b, c
[a-z] # range
* # 0 or more of previous
+ # 1 or more (ERE)
? # 0 or 1 (ERE)
{n,m} # n to m repetitions (ERE)
grep "^Error" log.txt # lines starting with "Error"
grep "ERROR$" log.txt # lines ending with "ERROR"
grep "^[A-Z]+:" data.txt # lines starting with uppercase letters then colon (ERE)
grep "[0-9]\{3\}-[0-9]\{4\}" file # phone-like (BRE)
grep -E "[0-9]{3}-[0-9]{4}" file # same with ERE
BRE (basic regex, default) requires backslashes for +, ?, {}, (). ERE (-E) doesn't. Always use -E unless you have a specific reason — cleaner syntax.
Word boundaries: -w
grep -w "cat" file.txt
# matches "cat" but not "category" or "scattered"
-w treats the pattern as a whole word — boundaries on both sides.
Quiet check: -q
if grep -q "ERROR" log.txt; then
echo "errors found"
fi
-q suppresses output; just sets the exit code (0 if found, 1 if not). Use for boolean checks.
Files with matches: -l (and -L)
grep -l "TODO" *.py # files containing TODO
grep -L "TODO" *.py # files NOT containing TODO
Just file names, no content. Useful for "find me files that mention X."
# Edit every file containing TODO
$EDITOR $(grep -l "TODO" *.py)
In pipes
ps aux | grep "node" | grep -v grep
ls -la | grep "^d" # only directories
grep in a pipeline is the most common usage. The grep -v grep idiom is to exclude the grep process itself when searching ps.
A cleaner alternative for ps:
pgrep node # PIDs of node
pgrep -af node # full command lines
Working with logs
# Last 100 errors
grep "ERROR" /var/log/system.log | tail -100
# Errors in the last hour (assuming RFC3339-ish dates)
grep "$(date -v-1H +%Y-%m-%dT%H)" /var/log/system.log | grep ERROR
# Unique error messages
grep "ERROR" log.txt | sort -u
# Most common errors (top 10)
grep "ERROR" log.txt | sort | uniq -c | sort -rn | head
Standard log analysis pipelines.
Color: --color
grep --color=auto "pattern" file.txt
Highlights matches in color. Add to your .zshrc:
alias grep='grep --color=auto'
auto means "color when stdout is a terminal, plain text when piped." Always-on (always) breaks pipelines.
Performance: ripgrep
brew install ripgrep
rg "pattern" .
rg (ripgrep) is a Rust-based grep replacement — way faster, smarter defaults (respects .gitignore), unicode-aware, simpler regex syntax.
For modern dev work, rg is the default. grep remains for portability and tiny scripts.
A code-search workflow
# Find all TODOs
rg TODO
# Find function definitions in Python
rg "^def " --type py
# Files that import a specific module
rg "from sqlalchemy" -l
# Replace across files (rg --files-with-matches + sed)
rg -l "old_function" | xargs sed -i '' 's/old_function/new_function/g'
For Python, JS, Go, anything — rg finds things fast.
Common stumbles
Forgetting -E. grep "(a|b)" matches the literal string (a|b). Use -E for regex.
Special chars in pattern. grep "$VAR" works for safe inputs, but if $VAR contains regex metacharacters (., *, [, \), they're interpreted. Use grep -F "$VAR" for literal.
Quote your pattern. grep $foo file — if $foo has spaces, splits into multiple args. Always grep "$foo" file.
Match a literal -. grep "-help" file is interpreted as a flag. Use grep -- "-help" file (-- ends flags) or grep -e "-help" file.
Recursive search excludes hidden by default? No. grep -r includes them. Use --exclude=".*" to skip.
grep in ps aux matches itself. Famous gotcha. Use pgrep or ps aux | grep '[n]ode' (the brackets prevent self-match).
Case-insensitive -i with -w. Both flags work together.
Newlines in patterns. grep is line-by-line; can't easily match across lines. Use pcre2grep (with -M) or scripted approaches.
What's next
Lesson 18: sed. Stream editor — replace, delete, insert, by line or pattern.
Recap
grep pattern file — find lines matching. -i case-insensitive, -n line numbers, -c count, -v invert, -r recursive, -w word match, -q quiet (exit code only), -l filenames only, -A/-B/-C context. -E for extended regex. -F for literal. --include/--exclude-dir to filter. For modern dev, ripgrep (rg) is the faster, smarter default.
Next lesson: sed.