Search text in files recursively
Searches every file under a directory for a text pattern and prints the matching line number, so you can jump straight to it in an editor.
Last updated
grep -rn "TODO" .
How it works
-r makes grep recurse into every subdirectory starting from the given path instead of only checking files you name explicitly; -n prefixes each match with its line number, which is what turns the output into something you can act on immediately (jump to that exact line) rather than just confirming a file contains the term somewhere. The search pattern comes first, the starting path last — quoting the pattern protects it from the shell if it contains spaces or characters that look like glob wildcards.
This is the everyday "where did I leave a TODO/FIXME/console.log" check across an entire project, needing nothing beyond grep itself.
Watch out for
- →grep's pattern is a regex by default (basic regular expressions unless you pass -E), so characters like ., *, [ have special meaning — escape them for a literal search, or add -F to force a fixed-string match with no regex interpretation at all.
- →--include="*.js" scopes a recursive search to matching files and --exclude-dir=node_modules skips noisy directories; both work on GNU grep and on macOS's BSD grep. The reasons to reach for ripgrep or GNU grep on macOS are speed and PCRE support, not these flags.
- →Recursing through a tree that includes .git or node_modules is slow and produces a lot of irrelevant noise — pair -r with --exclude-dir=.git --exclude-dir=node_modules (GNU grep) to skip them.
Part by part
- -rn
- Two flags in one argument. grep accepts them separately too, but this is the form that appears in practice.
- r
- Recurse into subdirectories. Without it grep expects files as arguments and complains that the directory is a directory.
- n
- Print line numbers. Cheap to add, and the difference between a result you can jump to and one you have to hunt for.
- "TODO"
- The pattern, quoted so the shell does not interpret it. Essential the moment the pattern contains a space, an asterisk or a dollar sign.
- .
- Where to search. Required with -r on most implementations, even though it looks like it should default to here.
Common mistakes
grep -rn TODO .Works for a single bare word and breaks the moment the pattern has a space or a shell metacharacter. Quoting always is the habit worth having rather than remembering when it matters.
grep -rn "TODO" . in a repository with node_modulesgrep has no concept of ignore files, so it searches every dependency and build artefact. The result is thousands of irrelevant matches and a long wait — use --exclude-dir, or ripgrep, which respects .gitignore.
grep -rn "a.b" .The dot is a regex metacharacter matching any character, so this also finds axb and a1b. Use -F for a literal search, or escape the dot, when the pattern is meant to be exact text.
Questions
How do I skip node_modules and .git?
--exclude-dir can be repeated: --exclude-dir=node_modules --exclude-dir=.git. Both GNU and BSD grep support it, despite occasional claims otherwise.
How do I search only certain file types?
--include takes a glob, as in --include="*.ts". Quote it so the shell passes the pattern through, and combine several by repeating the flag.
Should I use ripgrep instead?
For searching a codebase, usually yes. It respects .gitignore by default, is substantially faster, and needs no exclusion flags — which removes the most common cause of unusable grep output.
How do I see context around each match?
-A prints lines after, -B before, and -C both. grep -rn -C 2 "TODO" . gives two lines either side, which is usually enough to tell whether a match is the one you wanted.