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