Python’s fnmatch module matches filenames against Unix shell-style wildcard patterns. import fnmatch. fnmatch: fnmatch.fnmatch(name, pattern) → bool; case-insensitive on Windows, case-sensitive on Unix; patterns: * = any number of chars, ? = exactly one char, [seq] = char in seq, [!seq] = char not in seq. fnmatchcase: fnmatch.fnmatchcase(name, pattern) → always case-sensitive. filter: fnmatch.filter(names, pattern) → list of matching names from names iterable; equivalent to [n for n in names if fnmatch(n, pattern)]. translate: fnmatch.translate(pattern) → regex string corresponding to the shell pattern; compile with re.compile(fnmatch.translate(p)) for repeated use. Pattern examples: "*.py" matches Python files; "test_*.py" matches test modules; "[!._]*" excludes hidden/private files; "v[0-9].*" matches version files; "????.*" matches four-char basenames. Unlike glob, fnmatch operates on strings only — it does not touch the filesystem. Claude Code generates file filter pipelines, log file selectors, asset inclusion rules, and pattern-based routing tables.
CLAUDE.md for fnmatch
## fnmatch Stack
- Stdlib: import fnmatch
- Match: fnmatch.fnmatch("app.py", "*.py") # True
- Case: fnmatch.fnmatchcase("App.PY", "*.py") # False
- Filter: fnmatch.filter(names, "test_*.py")
- Regex: re.compile(fnmatch.translate("*.py")) # for repeated use
- Multi: any(fnmatch.fnmatch(n, p) for p in patterns)
fnmatch File Pattern Pipeline
# app/fnutil.py — pattern matching, filter, multi-pattern, rule engine
from __future__ import annotations
import fnmatch
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Iterable
# ─────────────────────────────────────────────────────────────────────────────
# 1. Pattern matching helpers
# ─────────────────────────────────────────────────────────────────────────────
def matches(name: str, pattern: str, case_sensitive: bool = True) -> bool:
"""
Return True if name matches the shell-style pattern.
Example:
matches("report_2025.csv", "report_*.csv") # True
matches("README.MD", "*.md", case_sensitive=False) # True
"""
if case_sensitive:
return fnmatch.fnmatchcase(name, pattern)
return fnmatch.fnmatch(name, pattern)
def matches_any(name: str, patterns: Iterable[str], case_sensitive: bool = True) -> bool:
"""
Return True if name matches any of the given patterns.
Example:
matches_any("app.py", ["*.py", "*.pyi"]) # True
matches_any("app.js", ["*.py", "*.pyi"]) # False
"""
fn = fnmatch.fnmatchcase if case_sensitive else fnmatch.fnmatch
return any(fn(name, p) for p in patterns)
def matches_all(name: str, patterns: Iterable[str], case_sensitive: bool = True) -> bool:
"""
Return True only if name matches every pattern in the list.
Example:
matches_all("test_app.py", ["test_*", "*.py"]) # True
matches_all("app.py", ["test_*", "*.py"]) # False
"""
fn = fnmatch.fnmatchcase if case_sensitive else fnmatch.fnmatch
return all(fn(name, p) for p in patterns)
def filter_names(names: Iterable[str], pattern: str, case_sensitive: bool = True) -> list[str]:
"""
Return names that match pattern.
Example:
filter_names(["app.py", "app.js", "test.py"], "*.py")
# ['app.py', 'test.py']
"""
if case_sensitive:
return [n for n in names if fnmatch.fnmatchcase(n, pattern)]
return fnmatch.filter(list(names), pattern)
def filter_multi(names: Iterable[str], patterns: Iterable[str], case_sensitive: bool = True) -> list[str]:
"""
Return names that match at least one of the patterns.
Example:
filter_multi(files, ["*.py", "*.pyi", "*.pyx"])
"""
pats = list(patterns)
return [n for n in names if matches_any(n, pats, case_sensitive=case_sensitive)]
def exclude_patterns(names: Iterable[str], patterns: Iterable[str], case_sensitive: bool = True) -> list[str]:
"""
Return names that do NOT match any of the exclude patterns.
Example:
exclude_patterns(files, ["*.pyc", "__pycache__", ".DS_Store"])
"""
pats = list(patterns)
return [n for n in names if not matches_any(n, pats, case_sensitive=case_sensitive)]
# ─────────────────────────────────────────────────────────────────────────────
# 2. Pattern compilation for repeated use
# ─────────────────────────────────────────────────────────────────────────────
class CompiledPattern:
"""
Pre-compiled fnmatch pattern for efficient repeated matching.
Example:
p = CompiledPattern("*.py")
p.matches("app.py") # True
p.filter(["a.py", "b.js"]) # ['a.py']
"""
def __init__(self, pattern: str, case_sensitive: bool = True) -> None:
self.pattern = pattern
self.case_sensitive = case_sensitive
flags = 0 if case_sensitive else re.IGNORECASE
self._regex = re.compile(fnmatch.translate(pattern), flags)
def matches(self, name: str) -> bool:
return bool(self._regex.match(name))
def filter(self, names: Iterable[str]) -> list[str]:
return [n for n in names if self._regex.match(n)]
def __repr__(self) -> str:
return f"CompiledPattern({self.pattern!r}, case_sensitive={self.case_sensitive})"
class PatternSet:
"""
A set of include and exclude patterns for file selection.
Example:
ps = PatternSet(include=["*.py", "*.pyi"], exclude=["test_*", "*_test.py"])
ps.matches("app.py") # True
ps.matches("test_app.py") # False
ps.filter(file_list)
"""
def __init__(
self,
include: list[str] | None = None,
exclude: list[str] | None = None,
case_sensitive: bool = True,
) -> None:
self._include = [CompiledPattern(p, case_sensitive) for p in (include or [])]
self._exclude = [CompiledPattern(p, case_sensitive) for p in (exclude or [])]
self._has_include = bool(self._include)
def matches(self, name: str) -> bool:
if any(ep.matches(name) for ep in self._exclude):
return False
if self._has_include:
return any(ip.matches(name) for ip in self._include)
return True
def filter(self, names: Iterable[str]) -> list[str]:
return [n for n in names if self.matches(n)]
# ─────────────────────────────────────────────────────────────────────────────
# 3. Filesystem helpers
# ─────────────────────────────────────────────────────────────────────────────
def list_matching(directory: str | Path, pattern: str, recursive: bool = False) -> list[Path]:
"""
List files in directory matching pattern (by basename).
Example:
list_matching("src", "*.py")
list_matching(".", "*.log", recursive=True)
"""
base = Path(directory)
if recursive:
return [p for p in base.rglob("*") if p.is_file() and fnmatch.fnmatchcase(p.name, pattern)]
return [p for p in base.iterdir() if p.is_file() and fnmatch.fnmatchcase(p.name, pattern)]
def list_matching_multi(
directory: str | Path,
patterns: list[str],
exclude: list[str] | None = None,
recursive: bool = False,
) -> list[Path]:
"""
List files matching any include pattern and not matching any exclude pattern.
Example:
list_matching_multi("src", ["*.py", "*.pyi"], exclude=["*_pb2.py"])
"""
base = Path(directory)
walk = base.rglob("*") if recursive else base.iterdir()
ps = PatternSet(include=patterns, exclude=exclude or [])
return [p for p in walk if p.is_file() and ps.matches(p.name)]
def find_by_stem(directory: str | Path, stem_pattern: str) -> list[Path]:
"""
Find files whose stem (name without extension) matches the given pattern.
Example:
find_by_stem("logs", "app_2025-??-??") # e.g. app_2025-03-15.log
"""
base = Path(directory)
return [
p for p in base.iterdir()
if p.is_file() and fnmatch.fnmatchcase(p.stem, stem_pattern)
]
# ─────────────────────────────────────────────────────────────────────────────
# 4. Pattern routing and dispatch
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class Route:
pattern: str
handler: Callable[[str], Any]
priority: int = 0
def matches(self, name: str) -> bool:
return fnmatch.fnmatchcase(name, self.pattern)
class PatternRouter:
"""
Route file names (or any strings) to handlers based on fnmatch patterns.
First matching route wins (by priority desc, then registration order).
Example:
router = PatternRouter()
router.add("*.log", handle_log, priority=10)
router.add("*.csv", handle_csv)
router.add("error_*", handle_error, priority=20)
router.route("error_app.log") # → handle_error
"""
def __init__(self) -> None:
self._routes: list[Route] = []
def add(self, pattern: str, handler: Callable[[str], Any], priority: int = 0) -> None:
self._routes.append(Route(pattern=pattern, handler=handler, priority=priority))
self._routes.sort(key=lambda r: -r.priority)
def route(self, name: str) -> Any:
for r in self._routes:
if r.matches(name):
return r.handler(name)
raise KeyError(f"No route matched: {name!r}")
def find_route(self, name: str) -> Route | None:
return next((r for r in self._routes if r.matches(name)), None)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Pattern utilities
# ─────────────────────────────────────────────────────────────────────────────
def pattern_to_regex(pattern: str, flags: int = 0) -> re.Pattern[str]:
"""
Convert a shell pattern to a compiled regex.
Example:
rx = pattern_to_regex("*.py")
rx.match("app.py") # Match object
"""
return re.compile(fnmatch.translate(pattern), flags)
def categorize(names: Iterable[str], categories: dict[str, list[str]]) -> dict[str, list[str]]:
"""
Assign each name to all matching categories.
categories: {category_name: [patterns]}
Example:
cats = categorize(
["app.py", "style.css", "index.html", "data.json"],
{"python": ["*.py", "*.pyi"], "web": ["*.html", "*.css", "*.js"]},
)
# {'python': ['app.py'], 'web': ['style.css', 'index.html']}
"""
result: dict[str, list[str]] = {cat: [] for cat in categories}
for name in names:
for cat, pats in categories.items():
if matches_any(name, pats):
result[cat].append(name)
return result
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
from typing import Any
print("=== fnmatch demo ===")
files = [
"app.py", "test_app.py", "app_test.py", "app.pyi", "app.js",
"README.md", "requirements.txt", "setup.py", "conftest.py",
"data.csv", "report_2025.csv", ".gitignore", "__pycache__",
]
print("\n--- matches / matches_any ---")
print(f" matches('app.py', '*.py') : {matches('app.py', '*.py')}")
print(f" matches_any('app.pyi', ['*.py','*.pyi']) : {matches_any('app.pyi', ['*.py','*.pyi'])}")
print(f" matches_all('test_app.py', ['test_*','*.py']): {matches_all('test_app.py', ['test_*','*.py'])}")
print("\n--- filter_names / filter_multi ---")
py_files = filter_names(files, "*.py")
print(f" *.py files: {py_files}")
src_files = filter_multi(files, ["*.py", "*.pyi", "*.js"])
print(f" src files: {src_files}")
clean = exclude_patterns(files, ["__pycache__", ".gitignore", "*.pyc"])
print(f" after exclusions: {clean}")
print("\n--- CompiledPattern ---")
p = CompiledPattern("test_*.py")
print(f" test_*.py matches: {p.filter(files)}")
print("\n--- PatternSet ---")
ps = PatternSet(include=["*.py", "*.pyi"], exclude=["test_*", "*_test.py", "conftest.py"])
print(f" include py, exclude tests: {ps.filter(files)}")
print("\n--- categorize ---")
cats = categorize(files, {
"python": ["*.py", "*.pyi"],
"data": ["*.csv", "*.json"],
"config": ["*.txt", "*.md", ".gitignore"],
})
for cat, matched in cats.items():
print(f" {cat}: {matched}")
print("\n--- PatternRouter ---")
router = PatternRouter()
router.add("test_*.py", lambda n: f"TEST:{n}", priority=20)
router.add("*.py", lambda n: f"SOURCE:{n}", priority=10)
router.add("*.csv", lambda n: f"DATA:{n}")
for fname in ["app.py", "test_app.py", "data.csv"]:
print(f" route({fname!r}) → {router.route(fname)}")
print("\n--- pattern_to_regex ---")
rx = pattern_to_regex("report_????-??-??.csv")
test_names = ["report_2025-01-15.csv", "report_2025.csv", "report_2025-01-15.log"]
for n in test_names:
print(f" {n!r:40s}: {bool(rx.match(n))}")
print("\n=== done ===")
For the glob alternative — glob.glob() and Path.glob() apply shell patterns directly to the filesystem and return matching file paths; fnmatch operates on plain strings without touching the filesystem — use glob when you need to discover files on disk; use fnmatch when you already have a list of names (from an archive, a database, an in-memory index, or a remote API listing) and need to filter it without performing filesystem I/O. For the re alternative — re provides full regular-expression power with groups, lookaheads, and anchors; fnmatch.translate() converts a shell pattern to a regex string, so the two are interoperable — use re directly when your matching rules are complex enough to need regex syntax; use fnmatch for the familiar *, ?, [seq] shell-pattern vocabulary that users of .gitignore files or shell globbing already know, since it is easier to read and configure without regex expertise. The Claude Skills 360 bundle includes fnmatch skill sets covering matches()/matches_any()/matches_all()/filter_names()/filter_multi()/exclude_patterns() core helpers, CompiledPattern and PatternSet classes with include/exclude support, list_matching()/list_matching_multi()/find_by_stem() filesystem utilities, PatternRouter for pattern-based dispatch, and categorize() for multi-category classification. Start with the free tier to try filename pattern matching and fnmatch pipeline code generation.