fnmatch

Matches a string against a shell wildcard pattern using *, ? and character classes.

Signature
fnmatch(string $pattern, string $filename, int $flags = 0): bool
  • Added in PHP 4.3.0

Parameters

ParameterTypeDescription
$patternstringThe shell wildcard pattern, using *, ?, [ ] ranges and \\ escapes.
$filenamestringThe string being tested. Usually a filename, but any string works.
$flagsoptionalint= 0Any 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, unless FNM_NOESCAPE is 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.