66from collections import namedtuple
77import re
88import typing
9+ from functools import partial
910
1011from .lrucache import LRUCache
1112from ._repr import make_repr
1718LineCounts = namedtuple ("LineCounts" , ["lines" , "non_blank" ])
1819
1920if typing .TYPE_CHECKING :
20- 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+ )
2131 from .base import FS
2232
2333
2636) # type: LRUCache[Tuple[Text, bool], Tuple[int, bool, Pattern]]
2737
2838
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+
2959def _translate (pattern , case_sensitive = True ):
3060 # type: (Text, bool) -> Text
3161 """Translate a wildcard pattern to a regular expression.
@@ -43,14 +73,14 @@ def _translate(pattern, case_sensitive=True):
4373 if not case_sensitive :
4474 pattern = pattern .lower ()
4575 i , n = 0 , len (pattern )
46- res = ""
76+ res = []
4777 while i < n :
4878 c = pattern [i ]
4979 i = i + 1
5080 if c == "*" :
51- res = res + "[^/]*"
81+ res . append ( "[^/]*" )
5282 elif c == "?" :
53- res = res + "."
83+ res . append ( "[^/]" )
5484 elif c == "[" :
5585 j = i
5686 if j < n and pattern [j ] == "!" :
@@ -60,28 +90,30 @@ def _translate(pattern, case_sensitive=True):
6090 while j < n and pattern [j ] != "]" :
6191 j = j + 1
6292 if j >= n :
63- res = res + "\\ ["
93+ res . append ( "\\ [" )
6494 else :
6595 stuff = pattern [i :j ].replace ("\\ " , "\\ \\ " )
6696 i = j + 1
6797 if stuff [0 ] == "!" :
6898 stuff = "^" + stuff [1 :]
6999 elif stuff [0 ] == "^" :
70100 stuff = "\\ " + stuff
71- res = "%s [%s]" % ( res , stuff )
101+ res . append ( " [%s]" % stuff )
72102 else :
73- res = res + re .escape (c )
74- return res
103+ res . append ( re .escape (c ) )
104+ return "" . join ( res )
75105
76106
77107def _translate_glob (pattern , case_sensitive = True ):
78108 levels = 0
79109 recursive = False
80110 re_patterns = ["" ]
81111 for component in iteratepath (pattern ):
82- if component == "**" :
83- re_patterns .append (".*/?" )
112+ if "**" in component :
84113 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 ))
85117 else :
86118 re_patterns .append (
87119 "/" + _translate (component , case_sensitive = case_sensitive )
@@ -141,6 +173,88 @@ def imatch(pattern, path):
141173 return bool (re_pattern .match (path ))
142174
143175
176+ def match_any (patterns , path ):
177+ # type: (Iterable[Text], Text) -> bool
178+ """Test if a path matches any of a list of patterns.
179+
180+ Will return `True` if ``patterns`` is an empty list.
181+
182+ Arguments:
183+ patterns (list): A list of wildcard pattern, e.g ``["*.py",
184+ "*.pyc"]``
185+ name (str): A filename.
186+
187+ Returns:
188+ bool: `True` if the path matches at least one of the patterns.
189+
190+ """
191+ if not patterns :
192+ return True
193+ return any (match (pattern , path ) for pattern in patterns )
194+
195+
196+ def imatch_any (patterns , path ):
197+ # type: (Iterable[Text], Text) -> bool
198+ """Test if a path matches any of a list of patterns (case insensitive).
199+
200+ Will return `True` if ``patterns`` is an empty list.
201+
202+ Arguments:
203+ patterns (list): A list of wildcard pattern, e.g ``["*.py",
204+ "*.pyc"]``
205+ name (str): A filename.
206+
207+ Returns:
208+ bool: `True` if the path matches at least one of the patterns.
209+
210+ """
211+ if not patterns :
212+ return True
213+ return any (imatch (pattern , path ) for pattern in patterns )
214+
215+
216+ def get_matcher (patterns , case_sensitive , accept_prefix = False ):
217+ # type: (Iterable[Text], bool, bool) -> Callable[[Text], bool]
218+ """Get a callable that matches paths against the given patterns.
219+
220+ Arguments:
221+ patterns (list): A list of wildcard pattern. e.g. ``["*.py",
222+ "*.pyc"]``
223+ case_sensitive (bool): If ``True``, then the callable will be case
224+ sensitive, otherwise it will be case insensitive.
225+ accept_prefix (bool): If ``True``, the name is
226+ not required to match the wildcards themselves
227+ but only need to be a prefix of a string that does.
228+
229+ Returns:
230+ callable: a matcher that will return `True` if the paths given as
231+ an argument matches any of the given patterns.
232+
233+ Example:
234+ >>> from fs import wildcard
235+ >>> is_python = wildcard.get_matcher(['*.py'], True)
236+ >>> is_python('__init__.py')
237+ True
238+ >>> is_python('foo.txt')
239+ False
240+
241+ """
242+ if not patterns :
243+ return lambda name : True
244+
245+ if accept_prefix :
246+ new_patterns = []
247+ for pattern in patterns :
248+ split = _split_pattern_by_rec (pattern )
249+ for i in range (len (split )):
250+ new_pattern = "/" .join (split [: i + 1 ])
251+ new_patterns .append (new_pattern )
252+ patterns = new_patterns
253+
254+ matcher = match_any if case_sensitive else imatch_any
255+ return partial (matcher , patterns )
256+
257+
144258class Globber (object ):
145259 """A generator of glob results."""
146260
0 commit comments