TechEarl

How to List Only Filenames with grep -l

grep -l prints the name of each file that contains a match and stops reading at the first hit, which makes it the fast answer to 'which files contain this string'. The lowercase -l, the inverted -L for files missing a pattern, the grep -rl one-liner, the NUL-safe xargs pipeline for find-and-replace, and the BSD vs GNU notes.

Ishan KarunaratneIshan Karunaratne⏱️ 10 min readUpdated
Use grep -l 'pattern' files to list only the filenames that contain a match. The inverted grep -L, the recursive grep -rl one-liner, the NUL-safe xargs pipeline for find-and-replace, and the macOS BSD vs GNU notes.

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

Try it with your own 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.

bash· Linux (GNU)
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.

bash· Linux (GNU)
grep -l ':pattern' *.log

That 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.

bash· Linux (GNU)
grep -rL ':pattern' :search_path

The audit use case is the obvious one. To find every file in a tree that is missing a copyright header:

bash
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:

bash
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:

bash
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:

bash· Linux (GNU)
grep -rl --include='*.ts' --exclude-dir=node_modules ':pattern' :search_path

BSD 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.

FeatureGNU grepBSD grep (macOS default)
-l (list matching files)SupportedSupported
-L (list non-matching files)SupportedSupported
-Z (NUL-separate filenames)SupportedSupported
-r recursiveSupportedSupported
--include / --excludeSupportedNot supported
--exclude-dirSupportedNot supported
-r follows symlinksDoes NOT followFollows 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 -l entirely. Add -n for line numbers and -H to force the filename prefix.
  • You need per-file match counts. Use -c. It prints filename:count for every file, and count of zero for files with no match. The grep count matches article covers the counting flags in depth.
  • You only want files with many matches. -l cannot threshold. Pipe grep -c output through awk -F: '$2 > 5' instead.
  • You are looking inside one file. -l on a single file just prints that file's name back at you if it matches, which is rarely useful. -l earns its keep across many files.

See also

FAQ

TagsgrepCLILinuxmacOSBSDxargsShell Scripting
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

Search multiple patterns with grep: grep -e 'A' -e 'B', grep -E 'A|B' alternation, and grep -f patterns.txt. Covers -F fixed strings, AND logic with chained greps and PCRE lookahead, and BSD vs GNU differences on macOS.

How to Search Multiple Patterns with grep

grep can OR several patterns three ways: -e per pattern, -E with alternation, or -f reading the list from a file. The one-liner is grep -E 'ERROR|WARN|FATAL' file. Here is when to pick each, how -F speeds up literal multi-pattern search, why grep has no single-pass AND, and the BSD vs GNU differences that bite on macOS.

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

How to Match a Domain Name with Regex

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

Match an email address with regex. Practical pattern, strict RFC 5321 pattern, JavaScript / Python / PHP examples, edge cases, engine compatibility, common mistakes, and a test table.

How to Match an Email Address with Regex

Match an email address with regex. The practical pattern, the strict RFC 5321 pattern, examples in JavaScript, Python, and PHP, edge cases, engine compatibility, common mistakes, and a validation test table.