TechEarl

How to Find Files by Extension (One or Many) with find

find . -type f -name '*.txt' lists every file with one extension. For many extensions you group -name tests with escaped parens and join them with -o. This covers the single one-liner, the multi-extension OR pattern, why the parens are mandatory, case-insensitive -iname, files with no extension at all, the -regex shortcut, and the BSD vs GNU divergence that bites on macOS.

Ishan KarunaratneIshan Karunaratne⏱️ 12 min readUpdated
Use find -type f -name '*.txt' for one extension, or group -name tests in escaped parens joined by -o for many (.jpg, .png, .gif). Case-insensitive -iname, files with no extension, the -regex shortcut, and BSD vs GNU find differences.

find . -type f -name '*.txt' lists every .txt file under the current directory. That single line covers most of what people mean by "find files by extension". The pattern is a glob (*.txt), the -type f keeps directories out of the results, and find walks the tree recursively by default.

The interesting cases start when you want more than one extension. "Find all jpg and png files" is not just two -name flags stuck together. find has its own operator-precedence rules, and getting them wrong silently returns the wrong set of files instead of erroring out. This page is the reference I keep open when I write batch-convert and asset-audit scripts.

Set your values

Try it with your own values

Set your OS, search path, and extension. The single-extension commands below update with your values.

The one-liner: a single extension

bash· Linux (GNU)
find :search_path -type f -name '*.:{ext}'

The *.txt pattern is a shell-style glob, not a regex: * matches any run of characters, so *.txt matches notes.txt, 2026-report.txt, and .txt itself. Quoting the pattern is mandatory (more on that in the mistakes section). Drop -type f only if you genuinely want directories named something.txt in the results, which is rare.

-name matches the basename of each path, never the directory part. find . -type f -name '*.txt' finds .txt files at any depth; the recursion is built in.

Many extensions: the OR pattern with grouping parens

To match several extensions, join -name tests with -o (the OR operator) and wrap the whole group in escaped parentheses:

bash
find . -type f \( -name '*.jpg' -o -name '*.png' -o -name '*.gif' \)

That returns every regular file whose name ends in .jpg, .png, or .gif. Add as many -o -name '...' clauses as you need inside the parens.

The \( \) are escaped because bare ( and ) are shell metacharacters. You can also single-quote them, '(' ... ')', which some people find cleaner. Either way, find sees literal parentheses and treats them as a grouping expression.

Why the escaped parens are mandatory

This is the part that silently breaks scripts. find joins adjacent tests with an implicit AND, and AND binds tighter than -o. Drop the parens and the command:

bash
find . -type f -name '*.jpg' -o -name '*.png'

parses as ( -type f AND -name '*.jpg' ) OR ( -name '*.png' ). The -type f only applies to the first branch. The second branch, -name '*.png', has no -type f guard, so directories named *.png slip into the results. Worse, if you append an action like -delete or -print, the action also binds to only part of the expression.

The parens force the grouping you actually want: -type f AND ( name matches one of these ). The rule of thumb: any time you use -o, wrap the OR group in \( \) and keep shared tests like -type f outside it.

Case-insensitive matching with -iname

-name is case-sensitive. A photo saved as IMG_1234.JPG will not match -name '*.jpg'. Swap in -iname for case-insensitive matching:

bash· Linux (GNU)
find :search_path -type f -iname '*.:{ext}'

-iname '*.jpg' matches .jpg, .JPG, .Jpg, and .jPg all the same. For the multi-extension case, use -iname inside the group:

bash
find . -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \)

Camera files and files copied off Windows or older systems often carry uppercase extensions, so -iname is the safer default for any image or media audit. PowerShell's -Filter is case-insensitive already, matching the Windows filesystem.

Find files with no extension at all

The inverse query: every file whose name has no dot in it. Use -not -name '*.*':

bash· Linux (GNU)
find :search_path -type f -not -name '*.*'

*.* is "anything, a dot, anything", and -not (synonym !) negates it. This is how you locate extensionless shell scripts, Makefile, Dockerfile, README, and binaries with no suffix.

One subtlety: a dotfile like .gitignore has a leading dot and does match *.*, so it counts as having an extension here. If you want extensionless files but still want to keep dotfiles, that takes a -regex filter instead, covered below.

Combine an extension with another filter

Extension is just one test. Stack it with size, time, or path tests and find ANDs them together.

Files of one extension, larger than 10 MB:

bash· Linux (GNU)
find :search_path -type f -name '*.:{ext}' -size +10M

Files of one extension, modified in the last 7 days:

bash· Linux (GNU)
find :search_path -type f -name '*.:{ext}' -mtime -7

When you mix a multi-extension OR group with another test, the group still needs its parens. The shared filter sits outside:

bash
find . -type f \( -name '*.jpg' -o -name '*.png' \) -size +500k

That reads as -type f AND ( jpg OR png ) AND -size +500k, which is the precedence you want.

The -regex alternative for many extensions

When the list of extensions gets long, the chain of -o -name clauses gets noisy. -regex matches a single pattern against the whole path, so you can express the alternation once:

bash
find . -regex '.*\.\(jpe?g\|png\|gif\)'

A few things to know about -regex:

  • It matches the entire path, not the basename, which is why the pattern starts with .*.
  • The default flavor on GNU find is Emacs-style POSIX, where the alternation group is \( \) and the OR is \|. The ? in jpe?g is \? in that flavor, so jpe?g should be written jpe\?g on GNU, or you can switch flavors with -regextype (see the table below).
  • BSD find uses basic POSIX regex by default and has no -regextype flag, so the same pattern can need adjusting between platforms.

For two or three extensions, the -name OR group is clearer and more portable. Reach for -regex when the alternation list is genuinely long or you need a pattern more complex than a suffix. The tradeoffs between the two are the subject of the find -regex vs -name deep dive.

macOS BSD vs GNU find

Extension matching with -name and -iname behaves the same on both. The divergence is entirely in -regex.

BehaviorGNU findBSD find (macOS default)
-name / -iname glob matchSameSame
-not / ! negationSupportedSupported
\( \) groupingSupportedSupported
-regex default flavorEmacs-style POSIX BREBasic POSIX
-regextype flagSupported (-regextype posix-extended, etc.)Not supported
-iregex (case-insensitive regex)SupportedSupported, different default flavor
-E extended-regex flagNot supported (use -regextype)Supported (find -E . -regex ...)

Practical upshot: if a script only uses -name / -iname extension globs, it runs unchanged on Linux and macOS. The moment it uses -regex, you have to account for the flavor difference. On macOS, find -E . -regex '.*\.(jpe?g|png|gif)' enables extended regex so the parens and | are unescaped; on GNU, the equivalent is find . -regextype posix-extended -regex '.*\.(jpe?g|png|gif)'. This is why I keep multi-extension matching on the -name OR pattern for anything that ships cross-platform.

Common mistakes

1. Forgetting to quote the glob. find . -name *.txt lets the shell expand *.txt before find ever sees it. If exactly one .txt file sits in the current directory, that filename gets passed as the pattern; if several match, find errors with "paths must precede expression"; if none match, the literal *.txt is passed through and it happens to work, which teaches the wrong lesson. Always quote: -name '*.txt'.

2. Dropping the grouping parens around an OR. Covered above. find . -type f -name '*.jpg' -o -name '*.png' does not do what it looks like. The -type f only guards the first branch. Always wrap the -o group in \( \).

3. Putting a slash in the pattern. -name 'src/*.ts' never matches, because -name tests the basename and basenames never contain a slash. To restrict by directory, use -path '*/src/*.ts', or combine -name '*.ts' with a separate -path filter.

4. Expecting *.txt to be a regex. Inside -name, the pattern is a glob. . is a literal dot, * matches any run including the empty string. If you write -name '.*txt' expecting "ends in txt", you get "starts with a dot", which is wrong. Globs and regex use the same characters with different meanings.

5. Mixing -iname and -name in one group by accident. If half the -o clauses use -name and half use -iname, the case-sensitivity is inconsistent and the result set looks arbitrary. Pick one for the whole group.

6. Assuming -regex matches the basename. It matches the full path, so a pattern without a leading .* matches almost nothing. This trips up everyone moving from -name to -regex for the first time.

When NOT to use this

A find extension match is the right tool most of the time, but reach for something else when:

  • You are inside a Git repository and only care about tracked files. git ls-files '*.js' is faster and skips node_modules, build output, and anything in .gitignore automatically. find walks everything unless you add -prune filters.
  • You need the fastest possible name lookup on a known filesystem. locate '*.txt' (backed by an updatedb index) returns instantly but only reflects the last index run, so it misses files created since. Good for "where did I put that", wrong for scripts that need current state.
  • You want to match by file content type, not the name suffix. An extension is just a naming convention; a file called data.txt can hold a JPEG. If you need real type detection, pipe matches through file (find . -type f -exec file {} +) and filter on its output.
  • You are matching one extension in one flat directory. A plain glob, ls *.txt or for f in *.txt, is simpler than find when there is no recursion and no extra filtering. find earns its keep with depth and composed tests.

See also

FAQ

TagsfindCLILinuxmacOSBSDPowerShellShell ScriptingGlob
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

Use find -user, -group, and -perm to locate files by ownership and mode. The -perm -mode vs -perm /mode distinction explained, world-writable and SUID/SGID audit recipes, orphaned-file checks, and the BSD vs GNU find differences on macOS.

How to Find Files by Owner, Group, or Permission with find

find -user www-data lists every file owned by a user; -group developers filters by group; -perm matches the mode bits. The subtle part is -perm -mode (all of these bits set) versus -perm /mode (any of these bits set). Plus the security-audit recipes for world-writable files and SUID/SGID binaries, the BSD vs GNU divergences, and the orphaned-file checks.

Use find -size +100M to list files larger than 100 megabytes. Unit suffixes (c/k/M/G), +/- sign convention, combine with sort -rn to surface the biggest files on disk, and BSD vs GNU rendering differences.

How to Find Files Larger Than a Size with find -size

find . -size +100M lists every file larger than 100 megabytes. The unit suffixes (c, k, M, G), the +/- sign convention, how to combine with sort to find the biggest files on disk, the BSD vs GNU divergence for printing sizes, and the wc -c trick for byte-exact thresholds.

Use find ... -print0 | xargs -0 grep -l 'PATTERN' to find every file containing a string. When grep -r is enough, when to add find as a pre-filter for performance, multi-pattern matching with -E, and the safe NUL-delimited pipeline.

How to Find Files Containing Specific Text (find + grep)

find ... -print0 | xargs -0 grep -l 'PATTERN' finds every file containing a piece of text. The combo handles weird filenames, scales to huge trees, and replaces three other common but broken pipelines. When to use grep -r alone, when to add find as a pre-filter, and the BSD vs GNU pitfalls.