Grep recursively with context lines
Search a directory tree for a pattern and print N lines of surrounding context for each match — for code archaeology, log spelunking, or config-file forensics.
How to grep recursively with context lines in each shell
grep -rn -A 3 -B 3 "TODO" .`-r` recurses. `-n` shows line numbers. `-A 3` prints 3 lines AFTER each match; `-B 3` prints 3 BEFORE; `-C 3` is shorthand for both. Match groups across files separated by `--`. Add `--include="*.py"` to limit to a glob; `--exclude-dir=node_modules` to skip directories. For massive trees, `ripgrep` (`rg`, `brew install ripgrep`) is dramatically faster — `rg -C 3 TODO` does the same and respects `.gitignore` automatically.
grep -rn -A 3 -B 3 "TODO" .Same external. The `rg` (ripgrep) ecosystem is the modern answer — `rg -C 3 -t py TODO` filters by file type by name (knows `py` = `*.py`, `*.pyi`, etc.). For fancy interactive context-grep in terminal, `fzf` integration: `rg --color=always -C 3 TODO | fzf --ansi`.
grep -rn -A 3 -B 3 "TODO" .Same external. Fish-style: `set matches (grep -rn -C 3 "TODO" .)`. For pattern lists from a file: `grep -rnf patterns.txt -C 3 .` (one pattern per line in patterns.txt). For "show match plus 10 lines AFTER, then stop" (limit per file): `grep -rn -A 10 -m 1 "TODO" .`.
Get-ChildItem -Recurse -File | Select-String -Pattern "TODO" -Context 3,3`-Context before,after` is the equivalent of `grep -B -A`. `-Context 3,3` is symmetric (3 before + 3 after). The output is a `[MatchInfo]` object — `.Context.PreContext` and `.Context.PostContext` are string arrays of the surrounding lines. For colourised terminal output, pipe through `Out-String` or use `Select-String` directly (it formats automatically). Limit by extension: `Get-ChildItem -Recurse -File -Filter *.py | Select-String ...`.
findstr /S /N /C:"TODO" *`/S` recurses; `/N` adds line numbers. cmd `findstr` has **NO context-lines flag** — it prints match-line only. For context: shell out to pwsh: `powershell -NoProfile -Command "Get-ChildItem -Recurse | Select-String TODO -Context 3,3"`. Or install ripgrep: `winget install BurntSushi.ripgrep.MSVC` then `rg -C 3 TODO`.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- For source code search across large repos (millions of lines), ripgrep / silver-searcher (`ag`) / git grep (`git grep -C 3 TODO`) are 10-100× faster than grep and respect `.gitignore` automatically — they skip `node_modules`, `target/`, `dist/`, etc., without the `--exclude-dir` boilerplate. Default to `rg` if it's installed; fall back to `grep -r` otherwise.
- `grep -r` (recursive) follows symlinks by default on GNU; `grep -R` follows them; `grep -r` does NOT follow symlinks on BSD (macOS). Symlink loops can hang the search forever on GNU `-R`; add `--exclude-dir=` to skip known link directories. Use `find . -type f -name "*.py" | xargs grep TODO` for finer control over what gets searched.
- For binary files mixed into the tree, `grep -r` will scan them and either skip (newer GNU grep treats binary as "no match") or dump unreadable output. Add `--binary-files=without-match` (or `-I` on GNU) to skip binaries entirely. `rg` skips binaries by default.
- pwsh `Select-String` output object structure is its strength — you can post-filter: `... | Where-Object { $_.Line.Length -lt 200 }` (skip minified lines). You can group by file: `... | Group-Object Path`. Plain-text `grep` output loses this structure; you'd have to awk it back.
Related commands
Related tasks
- Find and replace text in files— Substitute one string for another inside a file (or every file in a tree), in place.
- Pipe command output to grep— Filter the stdout of one command through a pattern matcher and print only matching lines.
- Replace a string across multiple files— Apply the same find-and-replace operation to a batch of files (filtered by glob, by content, or by recursion) — for renaming an API, fixing a typo across a codebase, or updating a config value.