grep -l 'pattern' files prints the name of each file that contains at least one match, instead of printing the matching lines. The lowercase -l is the flag for the question "which files contain this string?" rather than "show me the matches". It is also faster than a plain grep, because once a file produces a single match grep stops reading that file and moves on.
The capital -L inverts it: grep -L 'pattern' files lists every file that does not contain the pattern. That is the flag for audits, like "which source files are missing the license header".
Set your values
Set your OS, search path, and pattern. Every grep example below updates with your values.
The one-liner
The command people actually want is the recursive form: search a whole tree and print the path of every file with a match.
grep -rl ':pattern' :search_path-r is recursive, -l collapses each file down to its name. One line per matching file, no duplicates, no match text. On Windows, Select-String returns one result object per matching line, so Select-Object -ExpandProperty Path -Unique is what reduces it to one path per file.
What grep -l does (and why it is fast)
Plain grep reads every line of every file and prints each line that matches. grep -l only needs to answer a yes/no question per file: does this file contain the pattern at all? As soon as it finds the first match in a file, it stops reading that file and records the name.
That early exit is the speed win. For a 2 GB log file where the pattern appears on line 3, plain grep still scans all 2 GB looking for more matches; grep -l reads a few kilobytes and moves on. When the question is "which of these files contain X", -l is the correct tool, not just a formatting convenience.
grep -l ':pattern' *.logThat checks every .log file in the current directory and prints the names of the ones containing the pattern. No -r needed when you pass an explicit file glob.
grep -L: files that do NOT contain the pattern
Capital -L is the inverse. It lists files where the pattern is absent. This is not the same as grep -v (which inverts at the line level); -L inverts at the file level.
grep -rL ':pattern' :search_pathThe audit use case is the obvious one. To find every file in a tree that is missing a copyright header:
grep -rL 'Copyright' .Every path that command prints is a file with no Copyright line anywhere in it. Swap in whatever marker your audit needs: a license SPDX tag, a // @generated banner, a required import.
Piping the filename list into xargs
The classic reason to get a clean list of filenames is to feed it into another command. The find-and-replace-across-files pattern is grep -rl piped into xargs sed:
grep -rl 'old' . | xargs sed -i 's/old/new/g'grep -rl finds every file containing old, and xargs hands those names to sed -i, which edits each file in place. This only touches files that actually contain the string, so sed is not pointlessly rewriting files that would not change.
There is a trap in that pipeline: it breaks on filenames with spaces or newlines, because xargs splits on whitespace by default. The NUL-safe form fixes it. grep -rlZ separates filenames with a NUL byte instead of a newline, and xargs -0 splits on NUL:
grep -rlZ 'old' . | xargs -0 sed -i 's/old/new/g'Use the -Z plus -0 form by default. The plain version is fine for an interactive one-off in a directory you know has tidy filenames, but in a script it is a latent bug. On macOS, BSD sed -i needs an explicit backup suffix argument, so the in-place edit is sed -i '' 's/old/new/g' (empty string for no backup).
grep -l with --include and --exclude-dir
On GNU grep you can scope the recursive search before -l collapses it. --include restricts to a glob, --exclude-dir skips whole directories:
grep -rl --include='*.ts' --exclude-dir=node_modules ':pattern' :search_pathBSD grep on macOS has no --include or --exclude-dir, so the portable approach is find with -prune to drop the directory, piped into xargs grep -l. If you do this often on macOS, brew install grep gives you GNU grep as ggrep with the GNU flags. The grep cheat sheet has the full filename-filtering section.
macOS BSD grep vs GNU grep
For the listing flags specifically, the news is good: the core ones are portable.
| Feature | GNU grep | BSD grep (macOS default) |
|---|---|---|
-l (list matching files) | Supported | Supported |
-L (list non-matching files) | Supported | Supported |
-Z (NUL-separate filenames) | Supported | Supported |
-r recursive | Supported | Supported |
--include / --exclude | Supported | Not supported |
--exclude-dir | Supported | Not supported |
-r follows symlinks | Does NOT follow | Follows by default |
-l, -L, and -Z all work the same on both, so the grep -rlZ | xargs -0 pipeline is portable across Linux and macOS. The divergence is the --include / --exclude-dir family and the recursive symlink behavior. If a script depends on symlink handling, be explicit: GNU -R follows, GNU -r does not; BSD -r follows by default.
Common grep -l mistakes
1. The unsafe grep -rl | xargs on filenames with spaces. grep -rl 'x' . | xargs sed -i ... splits a path like my notes.txt into my and notes.txt, and sed then fails on two nonexistent files. Always use the NUL-safe grep -rlZ ... | xargs -0 ... form in scripts.
2. Confusing -l and -L. Lowercase -l lists files with a match; capital -L lists files without one. Mixed up, an audit silently reports the exact opposite set. A quick sanity check: run both and confirm the two lists do not overlap.
3. Confusing -L with -v. grep -v inverts at the line level (lines that do not match). grep -L inverts at the file level (files with zero matching lines). grep -vl is not the inverse of grep -l; it just lists files that have at least one non-matching line, which is almost every file.
4. Expecting -l to show match counts. -l is yes/no per file. It prints the name once and never tells you how many times the pattern appeared. For per-file counts you want -c, not -l.
5. Forgetting -r and passing a directory. grep -l 'x' somedir without -r prints grep: somedir: Is a directory instead of searching it. Either add -r, or pass an explicit file glob like somedir/*.log.
6. Quoting. An unquoted pattern with shell metacharacters (*, ?, [) gets expanded by the shell before grep sees it. Always single-quote the pattern.
When NOT to use -l
-l throws away information on purpose. Drop it when you need that information back:
- You need the matching lines. If the goal is to read the matches, not just locate the files, drop
-lentirely. Add-nfor line numbers and-Hto force the filename prefix. - You need per-file match counts. Use
-c. It printsfilename:countfor every file, andcountof zero for files with no match. The grep count matches article covers the counting flags in depth. - You only want files with many matches.
-lcannot threshold. Pipegrep -coutput throughawk -F: '$2 > 5'instead. - You are looking inside one file.
-lon a single file just prints that file's name back at you if it matches, which is rarely useful.-learns its keep across many files.
See also
- grep Command Cheat Sheet: the full grep reference, flags, regex modes, and BSD vs GNU differences
- Count matches with grep -c: when you need how many, not just which files
- Recursive search with grep -r: the recursion flags, symlink behavior, and directory filtering
- Find files containing text: the
findplusgrepapproach when you need to filter by filename first - External: GNU grep manual, FreeBSD grep(1) man page





