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
Set your OS, search path, and extension. The single-extension commands below update with your values.
The one-liner: a single extension
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:
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:
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:
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:
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 '*.*':
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:
find :search_path -type f -name '*.:{ext}' -size +10MFiles of one extension, modified in the last 7 days:
find :search_path -type f -name '*.:{ext}' -mtime -7When you mix a multi-extension OR group with another test, the group still needs its parens. The shared filter sits outside:
find . -type f \( -name '*.jpg' -o -name '*.png' \) -size +500kThat 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:
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?injpe?gis\?in that flavor, sojpe?gshould be writtenjpe\?gon GNU, or you can switch flavors with-regextype(see the table below). - BSD find uses basic POSIX regex by default and has no
-regextypeflag, 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.
| Behavior | GNU find | BSD find (macOS default) |
|---|---|---|
-name / -iname glob match | Same | Same |
-not / ! negation | Supported | Supported |
\( \) grouping | Supported | Supported |
-regex default flavor | Emacs-style POSIX BRE | Basic POSIX |
-regextype flag | Supported (-regextype posix-extended, etc.) | Not supported |
-iregex (case-insensitive regex) | Supported | Supported, different default flavor |
-E extended-regex flag | Not 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 skipsnode_modules, build output, and anything in.gitignoreautomatically.findwalks everything unless you add-prunefilters. - You need the fastest possible name lookup on a known filesystem.
locate '*.txt'(backed by anupdatedbindex) 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.txtcan hold a JPEG. If you need real type detection, pipe matches throughfile(find . -type f -exec file {} +) and filter on its output. - You are matching one extension in one flat directory. A plain glob,
ls *.txtorfor f in *.txt, is simpler thanfindwhen there is no recursion and no extra filtering.findearns its keep with depth and composed tests.
See also
- find Command Cheat Sheet: the full find reference covering name, type, size, time, perms, and
-execpatterns - Find files containing text (find + grep): once you have matched files by extension, search inside them
- find -regex vs -name: when the OR-of-globs pattern wins and when
-regexis worth the flavor headache - External: GNU findutils name-matching docs, FreeBSD find(1) man page.





