66from collections import namedtuple
77import re
88import typing
9+ from functools import partial
910
1011from .lrucache import LRUCache
1112from ._repr import make_repr
1213from .path import iteratepath
13- from . import wildcard
1414
1515
1616GlobMatch = namedtuple ("GlobMatch" , ["path" , "info" ])
1717Counts = namedtuple ("Counts" , ["files" , "directories" , "data" ])
1818LineCounts = namedtuple ("LineCounts" , ["lines" , "non_blank" ])
1919
2020if 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+ )
2231 from .base import FS
2332
2433
2736) # type: LRUCache[Tuple[Text, bool], Tuple[int, bool, Pattern]]
2837
2938
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+
30107def _translate_glob (pattern , case_sensitive = True ):
31108 levels = 0
32109 recursive = False
33110 re_patterns = ["" ]
34111 for component in iteratepath (pattern ):
35- if component == "**" :
36- re_patterns .append (".*/?" )
112+ if "**" in component :
37113 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 ))
38117 else :
39118 re_patterns .append (
40- "/" + wildcard . _translate (component , case_sensitive = case_sensitive )
119+ "/" + _translate (component , case_sensitive = case_sensitive )
41120 )
42121 levels += 1
43122 re_glob = "(?ms)^" + "" .join (re_patterns ) + ("/$" if pattern .endswith ("/" ) else "$" )
@@ -71,6 +150,8 @@ def match(pattern, path):
71150 except KeyError :
72151 levels , recursive , re_pattern = _translate_glob (pattern , case_sensitive = True )
73152 _PATTERN_CACHE [(pattern , True )] = (levels , recursive , re_pattern )
153+ if path and path [0 ] != "/" :
154+ path = "/" + path
74155 return bool (re_pattern .match (path ))
75156
76157
@@ -91,9 +172,95 @@ def imatch(pattern, path):
91172 except KeyError :
92173 levels , recursive , re_pattern = _translate_glob (pattern , case_sensitive = True )
93174 _PATTERN_CACHE [(pattern , False )] = (levels , recursive , re_pattern )
175+ if path and path [0 ] != "/" :
176+ path = "/" + path
94177 return bool (re_pattern .match (path ))
95178
96179
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+
97264class Globber (object ):
98265 """A generator of glob results."""
99266
0 commit comments