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