/commands/grep/grep-recursive-include
grep -r --include: search only certain file types recursively
- tool
- grep
- versions
- GNU grep 3.x
- os
- linux, macos, windows
grep -rn --include='*.js' 'pattern' . searches every .js file below the current directory and prints the file name and line number of each match. --include takes a glob matched against the file name; repeat it for several types, and use --exclude-dir to skip directories such as node_modules.
Example
mkdir -p src/lib node_modules/pkg
echo 'const apiUrl = "/v1";' > src/app.js
echo 'export const apiUrl = "/v2";' > src/lib/config.ts
echo 'apiUrl: /v3' > src/notes.md
echo 'var apiUrl = 1;' > node_modules/pkg/index.js
grep -rn --include='*.js' --include='*.ts' --exclude-dir=node_modules 'apiUrl' . | sortOutput
./src/app.js:1:const apiUrl = "/v1"; ./src/lib/config.ts:1:export const apiUrl = "/v2";
Quote the globs: unquoted, the shell may expand *.js against the current directory before grep sees it.
Useful variations
| Command | What it does |
|---|---|
| grep -rn 'TODO' src | Recursive search with line numbers |
| grep -rl 'TODO' src | Only list the files that match |
| grep -rL 'license' src | List the files that do not match |
| grep -rn --exclude='*.min.js' 'x' . | Skip files matching a glob |
| grep -rn --exclude-dir={.git,node_modules} 'x' . | Skip several directories (brace expansion is done by bash) |
| grep -rniw 'id' src | Case-insensitive, whole words only |
| grep -rI 'x' . | Skip binary files |
Count matches per file
mkdir -p src
printf 'TODO a\nTODO b\n' > src/a.py
printf 'TODO c\n' > src/b.py
printf 'done\n' > src/c.py
grep -rc --include='*.py' 'TODO' src | sortOutput
src/a.py:2 src/b.py:1 src/c.py:0
-c prints a count for every searched file, including files with 0 matches. Append | grep -v ':0$' to hide them.
Several file types: brace expansion vs a quoted glob
grep's globs have no {a,b} syntax. Unquoted, bash expands --include=*.{js,ts} into two --include options before grep runs, which works; quoted, grep receives the braces literally and nothing matches. The same happens with a glob that contains a directory:
mkdir -p src && echo 'apiUrl' > src/a.js && echo 'apiUrl' > src/b.ts
echo '--- unquoted braces (bash expands them):'
grep -rl --include=*.{js,ts} 'apiUrl' . | sort
echo '--- quoted braces:'
grep -rl --include='*.{js,ts}' 'apiUrl' . ; echo "exit $?"
echo '--- glob with a directory:'
grep -rl --include='src/*.js' 'apiUrl' . ; echo "exit $?"Output
--- unquoted braces (bash expands them): ./src/a.js ./src/b.ts --- quoted braces: exit 1 --- glob with a directory: exit 1
Exit status 1 means no line was selected, so a script cannot tell a wrong glob from a pattern that is genuinely absent. Check the glob on its own with grep -rl --include='*.js' '' . when a search unexpectedly finds nothing.
Options at a glance
| Option | Matches against | Notes |
|---|---|---|
| --include=GLOB | File base name | Search only files matching GLOB; repeat for several globs |
| --exclude=GLOB | File base name | Skip files matching GLOB; when --include and --exclude both match, the last one given wins |
| --exclude-dir=GLOB | Directory name | Skip directories during the recursive walk |
| --exclude-from=FILE | File base name | Read exclude globs from FILE, one per line |
| -r / -R | Directories | Recurse; -R also follows every symbolic link |
BSD grep on macOS supports -r, --include, --exclude and --exclude-dir with the same meaning. In a Git repository, git grep -n 'pattern' -- '*.js' searches only tracked files and skips node_modules without any option. See also grep --exclude-dir and, for matching several patterns at once, grep multiple patterns.
FAQ
Can --include match a directory path such as src/*.js?
No. --include and --exclude compare the glob with the file name only, so a pattern containing a slash matches nothing. Pass the directory as the search path instead, grep -rn --include='*.js' pattern src.
Does grep -r search the current directory when I give no path?
Yes with GNU grep: -r without a file argument searches the working directory. Older versions and BSD grep may read standard input instead, so pass . explicitly in scripts.
Is --include case-sensitive?
Yes. --include='*.js' does not match APP.JS. Add a second --include='*.JS' when such files exist.