fnmatch
Matches a string against a shell wildcard pattern using *, ? and character classes.
fnmatch(string $pattern, string $filename, int $flags = 0): bool- Added in PHP 4.3.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$pattern | string | The shell wildcard pattern, using *, ?, [ ] ranges and \\ escapes. |
$filename | string | The string being tested. Usually a filename, but any string works. |
$flagsoptional | int= 0 | Any combination of FNM_NOESCAPE, FNM_PATHNAME, FNM_PERIOD and FNM_CASEFOLD, combined with |. |
Return value
booltrue if the string matches the pattern, false otherwise.
Errors
Throws a ValueError if pattern or filename contains a null byte.
fnmatch answers a simple question: does this string match this shell-style pattern? It is the matching engine behind shell globbing, exposed as a pure string function — it never touches the filesystem, so the "filename" argument can be any string at all.
Pattern syntax
*matches zero or more characters.?matches exactly one character.[abc]and[a-z]match one character from a set or range;[!a-z]negates it.\escapes the next character, unlessFNM_NOESCAPEis set.
When to reach for it
Reach for it when you want glob-style matching without a regex, and especially when the patterns come from a user or a config file — *.log is far friendlier to write than /\.log$/, and a bad glob cannot blow up the way a bad regex can.
It pairs naturally with scandir or a directory iterator: list once, then filter the names in PHP. That is often better than glob(), which does its own filesystem pass per call.
What trips people up
Matching is case-sensitive by default; pass FNM_CASEFOLD if you want *.TXT to match notes.txt. And unlike a shell, * happily crosses / unless you add FNM_PATHNAME.
The function is not available on non-POSIX systems other than Windows, so guard with function_exists('fnmatch') in portable code.
Example
When to use it
- Filter a scandir result down to entries matching a user-supplied pattern like "report-*.csv".
- Check a request path against a list of glob rules to decide whether to skip authentication.
- Match a config key such as "cache.*.ttl" against a set of wildcard overrides.
Frequently asked questions
What is the difference between fnmatch and glob?
`glob()` searches the filesystem and returns matching paths, while `fnmatch()` only compares a pattern against a string you already have. Use `fnmatch` when the names come from somewhere other than a directory scan.
Is fnmatch case-sensitive?
Yes by default. Pass `FNM_CASEFOLD` as the third argument to match case-insensitively.
Can I use fnmatch on non-filename strings?
Yes — it is a pure string comparison and never touches the filesystem, so it works fine on URLs, route patterns, config keys or any other text.
More examples
Filtering a directory listing
The array contains `a.log`, `b.log` and `c.LOG` — the case-insensitive flag pulls in the uppercase one while `notes.txt` is dropped. This scandir-then-filter pattern gives you full control over the matching rules.
<?php
mkdir('fn-demo');
foreach (['a.log', 'b.log', 'notes.txt', 'c.LOG'] as $n) {
file_put_contents("fn-demo/$n", '');
}
$entries = array_diff(scandir('fn-demo'), ['.', '..']);
$logs = array_filter($entries, fn($n) => fnmatch('*.log', $n, FNM_CASEFOLD));
sort($logs);
print_r($logs);
foreach ($entries as $n) { unlink("fn-demo/$n"); }
rmdir('fn-demo');Character classes and negation
The three numbered files match `[0-9]` and fail `[!0-9]`; `fileX.dat` does the reverse. The `!` inside brackets is the glob negation operator — note it is `!`, not `^` as in a regex.
<?php
$names = ['file1.dat', 'file2.dat', 'file9.dat', 'fileX.dat'];
foreach ($names as $n) {
printf("%-12s digits:%-5s not-digit:%s\n",
$n,
var_export(fnmatch('file[0-9].dat', $n), true),
var_export(fnmatch('file[!0-9].dat', $n), true)
);
}Matching route patterns, not filenames
The first three paths match a rule and print `public`; `/admin` prints `protected`. Because `*` crosses slashes by default, `/assets/*` covers the whole subtree — that behaviour is exactly what you want here.
<?php
$publicRoutes = ['/health', '/assets/*', '/api/v?/status'];
foreach (['/health', '/assets/css/app.css', '/api/v2/status', '/admin'] as $path) {
$public = false;
foreach ($publicRoutes as $rule) {
if (fnmatch($rule, $path)) { $public = true; break; }
}
echo str_pad($path, 24), $public ? 'public' : 'protected', "\n";
}
Discussion