1"""Filename matching with shell patterns.
2
3fnmatch(FILENAME, PATTERN) matches according to the local convention.
4fnmatchcase(FILENAME, PATTERN) always takes case in account.
5
6The functions operate by translating the pattern into a regular
7expression.  They cache the compiled regular expressions for speed.
8
9The function translate(PATTERN) returns a regular expression
10corresponding to PATTERN.  (It does not compile it.)
11"""
12
13import re
14
15__all__ = ["fnmatch", "fnmatchcase", "translate"]
16
17_cache = {}
18_MAXCACHE = 100
19
20
21def _purge():
22    """Clear the pattern cache"""
23    _cache.clear()
24
25
26def fnmatch(name, pat):
27    """Test whether FILENAME matches PATTERN.
28
29    Patterns are Unix shell style:
30
31    *       matches everything
32    ?       matches any single character
33    [seq]   matches any character in seq
34    [!seq]  matches any char not in seq
35
36    An initial period in FILENAME is not special.
37    Both FILENAME and PATTERN are first case-normalized
38    if the operating system requires it.
39    If you don't want this, use fnmatchcase(FILENAME, PATTERN).
40    """
41
42    name = name.lower()
43    pat = pat.lower()
44    return fnmatchcase(name, pat)
45
46
47def fnmatchcase(name, pat):
48    """Test whether FILENAME matches PATTERN, including case.
49    This is a version of fnmatch() which doesn't case-normalize
50    its arguments.
51    """
52
53    try:
54        re_pat = _cache[pat]
55    except KeyError:
56        res = translate(pat)
57        if len(_cache) >= _MAXCACHE:
58            _cache.clear()
59        _cache[pat] = re_pat = re.compile(res)
60    return re_pat.match(name) is not None
61
62
63def translate(pat):
64    """Translate a shell PATTERN to a regular expression.
65
66    There is no way to quote meta-characters.
67    """
68    i, n = 0, len(pat)
69    res = '^'
70    while i < n:
71        c = pat[i]
72        i = i + 1
73        if c == '*':
74            if i < n and pat[i] == '*':
75                # is some flavor of "**"
76                i = i + 1
77                # Treat **/ as ** so eat the "/"
78                if i < n and pat[i] == '/':
79                    i = i + 1
80                if i >= n:
81                    # is "**EOF" - to align with .gitignore just accept all
82                    res = res + '.*'
83                else:
84                    # is "**"
85                    # Note that this allows for any # of /'s (even 0) because
86                    # the .* will eat everything, even /'s
87                    res = res + '(.*/)?'
88            else:
89                # is "*" so map it to anything but "/"
90                res = res + '[^/]*'
91        elif c == '?':
92            # "?" is any char except "/"
93            res = res + '[^/]'
94        elif c == '[':
95            j = i
96            if j < n and pat[j] == '!':
97                j = j + 1
98            if j < n and pat[j] == ']':
99                j = j + 1
100            while j < n and pat[j] != ']':
101                j = j + 1
102            if j >= n:
103                res = res + '\\['
104            else:
105                stuff = pat[i:j].replace('\\', '\\\\')
106                i = j + 1
107                if stuff[0] == '!':
108                    stuff = '^' + stuff[1:]
109                elif stuff[0] == '^':
110                    stuff = '\\' + stuff
111                res = f'{res}[{stuff}]'
112        else:
113            res = res + re.escape(c)
114
115    return res + '$'
116