44from __future__ import unicode_literals
55
66import typing
7+ from functools import partial
78
89import re
910from collections import namedtuple
1011
11- from . import wildcard
1212from ._repr import make_repr
1313from .lrucache import LRUCache
1414from .path import iteratepath
1515
16+
1617GlobMatch = namedtuple ("GlobMatch" , ["path" , "info" ])
1718Counts = namedtuple ("Counts" , ["files" , "directories" , "data" ])
1819LineCounts = namedtuple ("LineCounts" , ["lines" , "non_blank" ])
1920
2021if 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+ )
2332 from .base import FS
2433
2534
2837) # type: LRUCache[Tuple[Text, bool], Tuple[int, bool, Pattern]]
2938
3039
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+
31108def _translate_glob (pattern , case_sensitive = True ):
32109 levels = 0
33110 recursive = False
34111 re_patterns = ["" ]
35112 for component in iteratepath (pattern ):
36- if component == "**" :
37- re_patterns .append (".*/?" )
113+ if "**" in component :
38114 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 ))
39118 else :
40119 re_patterns .append (
41- "/" + wildcard . _translate (component , case_sensitive = case_sensitive )
120+ "/" + _translate (component , case_sensitive = case_sensitive )
42121 )
43122 levels += 1
44123 re_glob = "(?ms)^" + "" .join (re_patterns ) + ("/$" if pattern .endswith ("/" ) else "$" )
@@ -72,6 +151,8 @@ def match(pattern, path):
72151 except KeyError :
73152 levels , recursive , re_pattern = _translate_glob (pattern , case_sensitive = True )
74153 _PATTERN_CACHE [(pattern , True )] = (levels , recursive , re_pattern )
154+ if path and path [0 ] != "/" :
155+ path = "/" + path
75156 return bool (re_pattern .match (path ))
76157
77158
@@ -92,9 +173,95 @@ def imatch(pattern, path):
92173 except KeyError :
93174 levels , recursive , re_pattern = _translate_glob (pattern , case_sensitive = True )
94175 _PATTERN_CACHE [(pattern , False )] = (levels , recursive , re_pattern )
176+ if path and path [0 ] != "/" :
177+ path = "/" + path
95178 return bool (re_pattern .match (path ))
96179
97180
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+
98265class Globber (object ):
99266 """A generator of glob results."""
100267
0 commit comments