6
6
from collections import namedtuple
7
7
import re
8
8
import typing
9
+ from functools import partial
9
10
10
11
from .lrucache import LRUCache
11
12
from ._repr import make_repr
12
13
from .path import iteratepath
13
- from . import wildcard
14
14
15
15
16
16
GlobMatch = namedtuple ("GlobMatch" , ["path" , "info" ])
17
17
Counts = namedtuple ("Counts" , ["files" , "directories" , "data" ])
18
18
LineCounts = namedtuple ("LineCounts" , ["lines" , "non_blank" ])
19
19
20
20
if typing .TYPE_CHECKING :
21
- from typing import Iterator , List , Optional , Pattern , Text , Tuple
21
+ from typing import (
22
+ Iterator ,
23
+ List ,
24
+ Optional ,
25
+ Pattern ,
26
+ Text ,
27
+ Tuple ,
28
+ Iterable ,
29
+ Callable ,
30
+ )
22
31
from .base import FS
23
32
24
33
27
36
) # type: LRUCache[Tuple[Text, bool], Tuple[int, bool, Pattern]]
28
37
29
38
39
+ def _split_pattern_by_rec (pattern ):
40
+ # type: (Text) -> List[Text]
41
+ """Split a glob pattern at its directory seperators (/).
42
+
43
+ Takes into account escaped cases like [/].
44
+ """
45
+ indices = [- 1 ]
46
+ bracket_open = False
47
+ for i , c in enumerate (pattern ):
48
+ if c == "/" and not bracket_open :
49
+ indices .append (i )
50
+ elif c == "[" :
51
+ bracket_open = True
52
+ elif c == "]" :
53
+ bracket_open = False
54
+
55
+ indices .append (len (pattern ))
56
+ return [pattern [i + 1 : j ] for i , j in zip (indices [:- 1 ], indices [1 :])]
57
+
58
+
59
+ def _translate (pattern , case_sensitive = True ):
60
+ # type: (Text, bool) -> Text
61
+ """Translate a wildcard pattern to a regular expression.
62
+
63
+ There is no way to quote meta-characters.
64
+ Arguments:
65
+ pattern (str): A wildcard pattern.
66
+ case_sensitive (bool): Set to `False` to use a case
67
+ insensitive regex (default `True`).
68
+
69
+ Returns:
70
+ str: A regex equivalent to the given pattern.
71
+
72
+ """
73
+ if not case_sensitive :
74
+ pattern = pattern .lower ()
75
+ i , n = 0 , len (pattern )
76
+ res = []
77
+ while i < n :
78
+ c = pattern [i ]
79
+ i = i + 1
80
+ if c == "*" :
81
+ res .append ("[^/]*" )
82
+ elif c == "?" :
83
+ res .append ("[^/]" )
84
+ elif c == "[" :
85
+ j = i
86
+ if j < n and pattern [j ] == "!" :
87
+ j = j + 1
88
+ if j < n and pattern [j ] == "]" :
89
+ j = j + 1
90
+ while j < n and pattern [j ] != "]" :
91
+ j = j + 1
92
+ if j >= n :
93
+ res .append ("\\ [" )
94
+ else :
95
+ stuff = pattern [i :j ].replace ("\\ " , "\\ \\ " )
96
+ i = j + 1
97
+ if stuff [0 ] == "!" :
98
+ stuff = "^" + stuff [1 :]
99
+ elif stuff [0 ] == "^" :
100
+ stuff = "\\ " + stuff
101
+ res .append ("[%s]" % stuff )
102
+ else :
103
+ res .append (re .escape (c ))
104
+ return "" .join (res )
105
+
106
+
30
107
def _translate_glob (pattern , case_sensitive = True ):
31
108
levels = 0
32
109
recursive = False
33
110
re_patterns = ["" ]
34
111
for component in iteratepath (pattern ):
35
- if component == "**" :
36
- re_patterns .append (".*/?" )
112
+ if "**" in component :
37
113
recursive = True
114
+ split = component .split ("**" )
115
+ split_re = [_translate (s , case_sensitive = case_sensitive ) for s in split ]
116
+ re_patterns .append ("/?" + ".*/?" .join (split_re ))
38
117
else :
39
118
re_patterns .append (
40
- "/" + wildcard . _translate (component , case_sensitive = case_sensitive )
119
+ "/" + _translate (component , case_sensitive = case_sensitive )
41
120
)
42
121
levels += 1
43
122
re_glob = "(?ms)^" + "" .join (re_patterns ) + ("/$" if pattern .endswith ("/" ) else "$" )
@@ -71,6 +150,8 @@ def match(pattern, path):
71
150
except KeyError :
72
151
levels , recursive , re_pattern = _translate_glob (pattern , case_sensitive = True )
73
152
_PATTERN_CACHE [(pattern , True )] = (levels , recursive , re_pattern )
153
+ if path and path [0 ] != "/" :
154
+ path = "/" + path
74
155
return bool (re_pattern .match (path ))
75
156
76
157
@@ -91,9 +172,95 @@ def imatch(pattern, path):
91
172
except KeyError :
92
173
levels , recursive , re_pattern = _translate_glob (pattern , case_sensitive = True )
93
174
_PATTERN_CACHE [(pattern , False )] = (levels , recursive , re_pattern )
175
+ if path and path [0 ] != "/" :
176
+ path = "/" + path
94
177
return bool (re_pattern .match (path ))
95
178
96
179
180
+ def match_any (patterns , path ):
181
+ # type: (Iterable[Text], Text) -> bool
182
+ """Test if a path matches any of a list of patterns.
183
+
184
+ Will return `True` if ``patterns`` is an empty list.
185
+
186
+ Arguments:
187
+ patterns (list): A list of wildcard pattern, e.g ``["*.py",
188
+ "*.pyc"]``
189
+ name (str): A filename.
190
+
191
+ Returns:
192
+ bool: `True` if the path matches at least one of the patterns.
193
+
194
+ """
195
+ if not patterns :
196
+ return True
197
+ return any (match (pattern , path ) for pattern in patterns )
198
+
199
+
200
+ def imatch_any (patterns , path ):
201
+ # type: (Iterable[Text], Text) -> bool
202
+ """Test if a path matches any of a list of patterns (case insensitive).
203
+
204
+ Will return `True` if ``patterns`` is an empty list.
205
+
206
+ Arguments:
207
+ patterns (list): A list of wildcard pattern, e.g ``["*.py",
208
+ "*.pyc"]``
209
+ name (str): A filename.
210
+
211
+ Returns:
212
+ bool: `True` if the path matches at least one of the patterns.
213
+
214
+ """
215
+ if not patterns :
216
+ return True
217
+ return any (imatch (pattern , path ) for pattern in patterns )
218
+
219
+
220
+ def get_matcher (patterns , case_sensitive , accept_prefix = False ):
221
+ # type: (Iterable[Text], bool, bool) -> Callable[[Text], bool]
222
+ """Get a callable that matches paths against the given patterns.
223
+
224
+ Arguments:
225
+ patterns (list): A list of wildcard pattern. e.g. ``["*.py",
226
+ "*.pyc"]``
227
+ case_sensitive (bool): If ``True``, then the callable will be case
228
+ sensitive, otherwise it will be case insensitive.
229
+ accept_prefix (bool): If ``True``, the name is
230
+ not required to match the wildcards themselves
231
+ but only need to be a prefix of a string that does.
232
+
233
+ Returns:
234
+ callable: a matcher that will return `True` if the paths given as
235
+ an argument matches any of the given patterns.
236
+
237
+ Example:
238
+ >>> from fs import wildcard
239
+ >>> is_python = wildcard.get_matcher(['*.py'], True)
240
+ >>> is_python('__init__.py')
241
+ True
242
+ >>> is_python('foo.txt')
243
+ False
244
+
245
+ """
246
+ if not patterns :
247
+ return lambda name : True
248
+
249
+ if accept_prefix :
250
+ new_patterns = []
251
+ for pattern in patterns :
252
+ split = _split_pattern_by_rec (pattern )
253
+ for i in range (1 , len (split )):
254
+ new_pattern = "/" .join (split [:i ])
255
+ new_patterns .append (new_pattern )
256
+ new_patterns .append (new_pattern + "/" )
257
+ new_patterns .append (pattern )
258
+ patterns = new_patterns
259
+
260
+ matcher = match_any if case_sensitive else imatch_any
261
+ return partial (matcher , patterns )
262
+
263
+
97
264
class Globber (object ):
98
265
"""A generator of glob results."""
99
266
0 commit comments