1"""Match wildcard filenames.
2"""
3# Adapted from https://hg.python.org/cpython/file/2.7/Lib/fnmatch.py
4
5from __future__ import unicode_literals, print_function
6
7import re
8import typing
9from functools import partial
10
11from .lrucache import LRUCache
12
13if typing.TYPE_CHECKING:
14    from typing import Callable, Iterable, Text, Tuple, Pattern
15
16
17_PATTERN_CACHE = LRUCache(1000)  # type: LRUCache[Tuple[Text, bool], Pattern]
18
19
20def match(pattern, name):
21    # type: (Text, Text) -> bool
22    """Test whether a name matches a wildcard pattern.
23
24    Arguments:
25        pattern (str): A wildcard pattern, e.g. ``"*.py"``.
26        name (str): A filename.
27
28    Returns:
29        bool: `True` if the filename matches the pattern.
30
31    """
32    try:
33        re_pat = _PATTERN_CACHE[(pattern, True)]
34    except KeyError:
35        res = "(?ms)" + _translate(pattern) + r'\Z'
36        _PATTERN_CACHE[(pattern, True)] = re_pat = re.compile(res)
37    return re_pat.match(name) is not None
38
39
40def imatch(pattern, name):
41    # type: (Text, Text) -> bool
42    """Test whether a name matches a wildcard pattern (case insensitive).
43
44    Arguments:
45        pattern (str): A wildcard pattern, e.g. ``"*.py"``.
46        name (bool): A filename.
47
48    Returns:
49        bool: `True` if the filename matches the pattern.
50
51    """
52    try:
53        re_pat = _PATTERN_CACHE[(pattern, False)]
54    except KeyError:
55        res = "(?ms)" + _translate(pattern, case_sensitive=False) + r'\Z'
56        _PATTERN_CACHE[(pattern, False)] = re_pat = re.compile(res, re.IGNORECASE)
57    return re_pat.match(name) is not None
58
59
60def match_any(patterns, name):
61    # type: (Iterable[Text], Text) -> bool
62    """Test if a name matches any of a list of patterns.
63
64    Will return `True` if ``patterns`` is an empty list.
65
66    Arguments:
67        patterns (list): A list of wildcard pattern, e.g ``["*.py",
68            "*.pyc"]``
69        name (str): A filename.
70
71    Returns:
72        bool: `True` if the name matches at least one of the patterns.
73
74    """
75    if not patterns:
76        return True
77    return any(match(pattern, name) for pattern in patterns)
78
79
80def imatch_any(patterns, name):
81    # type: (Iterable[Text], Text) -> bool
82    """Test if a name matches any of a list of patterns (case insensitive).
83
84    Will return `True` if ``patterns`` is an empty list.
85
86    Arguments:
87        patterns (list): A list of wildcard pattern, e.g ``["*.py",
88            "*.pyc"]``
89        name (str): A filename.
90
91    Returns:
92        bool: `True` if the name matches at least one of the patterns.
93
94    """
95    if not patterns:
96        return True
97    return any(imatch(pattern, name) for pattern in patterns)
98
99
100def get_matcher(patterns, case_sensitive):
101    # type: (Iterable[Text], bool) -> Callable[[Text], bool]
102    """Get a callable that matches names against the given patterns.
103
104    Arguments:
105        patterns (list): A list of wildcard pattern. e.g. ``["*.py",
106            "*.pyc"]``
107        case_sensitive (bool): If ``True``, then the callable will be case
108            sensitive, otherwise it will be case insensitive.
109
110    Returns:
111        callable: a matcher that will return `True` if the name given as
112        an argument matches any of the given patterns.
113
114    Example:
115        >>> from fs import wildcard
116        >>> is_python = wildcard.get_matcher(['*.py'], True)
117        >>> is_python('__init__.py')
118        True
119        >>> is_python('foo.txt')
120        False
121
122    """
123    if not patterns:
124        return lambda name: True
125    if case_sensitive:
126        return partial(match_any, patterns)
127    else:
128        return partial(imatch_any, patterns)
129
130
131def _translate(pattern, case_sensitive=True):
132    # type: (Text, bool) -> Text
133    """Translate a wildcard pattern to a regular expression.
134
135    There is no way to quote meta-characters.
136
137    Arguments:
138        pattern (str): A wildcard pattern.
139        case_sensitive (bool): Set to `False` to use a case
140            insensitive regex (default `True`).
141
142    Returns:
143        str: A regex equivalent to the given pattern.
144
145    """
146    if not case_sensitive:
147        pattern = pattern.lower()
148    i, n = 0, len(pattern)
149    res = ""
150    while i < n:
151        c = pattern[i]
152        i = i + 1
153        if c == "*":
154            res = res + "[^/]*"
155        elif c == "?":
156            res = res + "."
157        elif c == "[":
158            j = i
159            if j < n and pattern[j] == "!":
160                j = j + 1
161            if j < n and pattern[j] == "]":
162                j = j + 1
163            while j < n and pattern[j] != "]":
164                j = j + 1
165            if j >= n:
166                res = res + "\\["
167            else:
168                stuff = pattern[i:j].replace("\\", "\\\\")
169                i = j + 1
170                if stuff[0] == "!":
171                    stuff = "^" + stuff[1:]
172                elif stuff[0] == "^":
173                    stuff = "\\" + stuff
174                res = "%s[%s]" % (res, stuff)
175        else:
176            res = res + re.escape(c)
177    return res
178