grep
recursive search
file extensions
command line
programming tips

How can I grep recursively, but only in files with certain extensions?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Use grep -r --include="*.ext" "pattern" /path to search recursively but only in files matching a specific extension. The --include flag tells grep to examine only files whose names match the given glob pattern, skipping everything else. This is the simplest and most efficient approach for extension-filtered recursive searches.

The --include Flag: The Direct Solution

bash
1# Search for "useState" only in .tsx files
2grep -r --include="*.tsx" "useState" ./src
3
4# Search for "TODO" in Python files
5grep -r --include="*.py" "TODO" .
6
7# Multiple extensions: use multiple --include flags
8grep -r --include="*.ts" --include="*.tsx" "interface User" ./src
9
10# Case-insensitive search in JavaScript files
11grep -ri --include="*.js" "fetchdata" ./src

The -r flag (or --recursive) tells grep to descend into subdirectories. The --include flag filters which files to search. Files that do not match the pattern are never opened, making this faster than grepping everything and filtering afterward.

Combining --include with --exclude-dir

In real projects, you almost always want to skip certain directories like node_modules, .git, or build output:

bash
1# Search TypeScript files, skip node_modules and dist
2grep -r \
3  --include="*.ts" \
4  --include="*.tsx" \
5  --exclude-dir=node_modules \
6  --exclude-dir=dist \
7  --exclude-dir=.git \
8  "import.*from" ./src
9
10# Skip multiple directories with a brace pattern (bash)
11grep -r \
12  --include="*.py" \
13  --exclude-dir={.venv,__pycache__,.git,build} \
14  "def " .

Without --exclude-dir, grep will descend into node_modules (which can contain hundreds of thousands of files) and produce both slow performance and irrelevant results.

Alternative: Using find with grep

The find + grep combination gives you more control over file selection, especially when you need criteria beyond just the extension (file size, modification time, permissions):

bash
1# Basic find + grep
2find ./src -type f -name "*.java" -exec grep -l "SQLException" {} +
3
4# Multiple extensions with find
5find . -type f \( -name "*.ts" -o -name "*.tsx" \) -exec grep -Hn "TODO" {} +

The -exec ... {} + syntax passes multiple filenames to grep at once (like xargs), which is faster than -exec ... {} \; that runs grep once per file.

Handling Filenames with Spaces

If your project contains filenames with spaces or special characters, use null-delimited output:

bash
find . -type f -name "*.md" -print0 | xargs -0 grep -l "deprecated"

The -print0 and -0 flags use null bytes as delimiters instead of newlines, correctly handling filenames like my file (copy).md.

Modern Alternatives: ripgrep, ag, and fd

For large codebases, specialized search tools are significantly faster than grep because they respect .gitignore, skip binary files, and use parallel processing:

ripgrep (rg)

bash
1# ripgrep with file type filter (auto-skips .git, node_modules, etc.)
2rg -t py "def " .
3
4# Multiple types
5rg -t ts -t tsx "interface" ./src
6
7# Using glob pattern
8rg --glob "*.java" "throws" ./src
9
10# List available type definitions
11rg --type-list | grep python

ripgrep is typically 2-10x faster than grep on large repositories.

The Silver Searcher (ag)

bash
1# ag with file type filter
2ag --python "import os"
3
4# ag with file extension glob
5ag -G "\.java$" "public class" ./src

fd + grep (modern find replacement)

bash
1# fd finds files, grep searches them
2fd -e py | xargs grep "def "
3
4# fd with multiple extensions
5fd -e ts -e tsx | xargs grep -l "useState"

Comparison of Approaches

ApproachSyntaxRespects .gitignoreSpeed on Large ReposHandles Spaces
grep -r --includegrep -r --include="*.py" "pat" .NoModerateYes
find + grepfind . -name "*.py" -exec grep "pat" {} +NoModerateWith -print0
ripgrep (rg)rg -t py "pat"YesFastYes
agag --python "pat"YesFastYes
fd + grepfd -e py | xargs grep "pat"YesFastWith -print0

These flags combine well with --include for practical code searching:

bash
1# -n: Show line numbers
2grep -rn --include="*.py" "class.*Error" .
3
4# -l: Show only filenames (not matching lines)
5grep -rl --include="*.java" "deprecated" ./src
6
7# -L: Show files that do NOT match
8grep -rL --include="*.py" "if __name__" ./src
9
10# -c: Count matches per file
11grep -rc --include="*.js" "console.log" ./src
12
13# -w: Match whole words only (avoids "toString" matching "String")
14grep -rw --include="*.java" "String" ./src
15
16# -A 3 -B 2: Show 3 lines after and 2 lines before each match
17grep -rn --include="*.py" -A 3 -B 2 "def process" .
18
19# -P: Use Perl-compatible regex (GNU grep)
20grep -rP --include="*.ts" "import\s+\{.*\}\s+from" ./src
21
22# -E: Extended regex (works on macOS and Linux)
23grep -rE --include="*.py" "def (get|set|delete)_" .

Searching for Multiple Patterns

bash
1# OR: match either pattern (extended regex)
2grep -rE --include="*.py" "import (os|sys|pathlib)" .
3
4# AND: both patterns in the same file (using two greps)
5grep -rl --include="*.java" "implements Serializable" . | \
6  xargs grep -l "transient"
7
8# NOT: exclude lines matching a pattern
9grep -r --include="*.conf" "server" . | grep -v "comment"

Creating a Shell Alias for Common Searches

If you search code frequently with the same extensions, create aliases:

bash
1# Add to ~/.bashrc or ~/.zshrc
2alias gpy='grep -rn --include="*.py" --exclude-dir={.venv,__pycache__,.git}'
3alias gjs='grep -rn --include="*.js" --include="*.ts" --include="*.tsx" --exclude-dir={node_modules,.git,dist}'
4alias gjava='grep -rn --include="*.java" --exclude-dir={target,.git,build}'
5
6# Usage
7gpy "import requests" .
8gjs "fetch(" ./src

Common Pitfalls

Forgetting to quote the glob pattern. Without quotes, the shell expands *.py against files in the current directory before grep sees it. Always write --include="*.py" with quotes.

Using -r vs -R. On GNU grep, -r does not follow symlinks while -R does. On macOS/BSD grep, -r follows symlinks. If your project has symlinked directories (common in monorepos), this distinction matters. Use -R explicitly if you want to follow symlinks.

Mixing up --include and --exclude order. grep processes --include and --exclude in the order given. If you write --exclude="*.test.ts" --include="*.ts", the include re-adds what the exclude removed. Put --include first, then --exclude.

Not excluding build directories. Searching without --exclude-dir in a JavaScript project scans node_modules, which can contain 50,000+ files. Always exclude node_modules, .git, build, dist, and other generated directories.

Expecting recursive search by default. Plain grep "pattern" . does not recurse. You must pass -r or -R. Omitting the flag searches only the specified path (and fails on directories with "Is a directory" error).

macOS grep limitations. macOS ships BSD grep, which lacks -P (Perl regex). For advanced regex patterns on macOS, install GNU grep via brew install grep (available as ggrep) or use ripgrep.

Summary

grep -r --include="*.ext" "pattern" . is the standard way to search recursively within specific file types. Combine it with --exclude-dir to skip generated directories, and add -n, -l, or -c flags depending on whether you need line numbers, filenames only, or match counts. For large codebases, consider ripgrep (rg) as a faster drop-in replacement that automatically respects .gitignore and skips binary files. Create shell aliases for your most common search patterns to save time on repeated lookups.


Course illustration
Course illustration

All Rights Reserved.