258 |
258 |
259 |
259 |
260 class FnmatchMatcher(object): |
260 class FnmatchMatcher(object): |
261 """A matcher for files by file name pattern.""" |
261 """A matcher for files by file name pattern.""" |
262 def __init__(self, pats): |
262 def __init__(self, pats): |
263 self.pats = pats[:] |
263 self.pats = list(pats) |
264 # fnmatch is platform-specific. On Windows, it does the Windows thing |
264 self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS) |
265 # of treating / and \ as equivalent. But on other platforms, we need to |
|
266 # take care of that ourselves. |
|
267 fnpats = (fnmatch.translate(p) for p in pats) |
|
268 # Python3.7 fnmatch translates "/" as "/", before that, it translates as "\/", |
|
269 # so we have to deal with maybe a backslash. |
|
270 fnpats = (re.sub(r"\\?/", r"[\\\\/]", p) for p in fnpats) |
|
271 flags = 0 |
|
272 if env.WINDOWS: |
|
273 # Windows is also case-insensitive, so make the regex case-insensitive. |
|
274 flags |= re.IGNORECASE |
|
275 self.re = re.compile(join_regex(fnpats), flags=flags) |
|
276 |
265 |
277 def __repr__(self): |
266 def __repr__(self): |
278 return "<FnmatchMatcher %r>" % self.pats |
267 return "<FnmatchMatcher %r>" % self.pats |
279 |
268 |
280 def info(self): |
269 def info(self): |
294 else: |
283 else: |
295 the_sep = os.sep |
284 the_sep = os.sep |
296 return the_sep |
285 return the_sep |
297 |
286 |
298 |
287 |
|
288 def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): |
|
289 """Convert fnmatch patterns to a compiled regex that matches any of them. |
|
290 |
|
291 Slashes are always converted to match either slash or backslash, for |
|
292 Windows support, even when running elsewhere. |
|
293 |
|
294 If `partial` is true, then the pattern will match if the target string |
|
295 starts with the pattern. Otherwise, it must match the entire string. |
|
296 |
|
297 Returns: a compiled regex object. Use the .match method to compare target |
|
298 strings. |
|
299 |
|
300 """ |
|
301 regexes = (fnmatch.translate(pattern) for pattern in patterns) |
|
302 # Python3.7 fnmatch translates "/" as "/". Before that, it translates as "\/", |
|
303 # so we have to deal with maybe a backslash. |
|
304 regexes = (re.sub(r"\\?/", r"[\\\\/]", regex) for regex in regexes) |
|
305 |
|
306 if partial: |
|
307 # fnmatch always adds a \Z to match the whole string, which we don't |
|
308 # want, so we remove the \Z. While removing it, we only replace \Z if |
|
309 # followed by paren (introducing flags), or at end, to keep from |
|
310 # destroying a literal \Z in the pattern. |
|
311 regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes) |
|
312 |
|
313 flags = 0 |
|
314 if case_insensitive: |
|
315 flags |= re.IGNORECASE |
|
316 compiled = re.compile(join_regex(regexes), flags=flags) |
|
317 |
|
318 return compiled |
|
319 |
|
320 |
299 class PathAliases(object): |
321 class PathAliases(object): |
300 """A collection of aliases for paths. |
322 """A collection of aliases for paths. |
301 |
323 |
302 When combining data files from remote machines, often the paths to source |
324 When combining data files from remote machines, often the paths to source |
303 code are different, for example, due to OS differences, or because of |
325 code are different, for example, due to OS differences, or because of |
341 if not pattern.startswith('*') and not isabs_anywhere(pattern): |
363 if not pattern.startswith('*') and not isabs_anywhere(pattern): |
342 pattern = abs_file(pattern) |
364 pattern = abs_file(pattern) |
343 if not pattern.endswith(pattern_sep): |
365 if not pattern.endswith(pattern_sep): |
344 pattern += pattern_sep |
366 pattern += pattern_sep |
345 |
367 |
346 # Make a regex from the pattern. fnmatch always adds a \Z to |
368 # Make a regex from the pattern. |
347 # match the whole string, which we don't want, so we remove the \Z. |
369 regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) |
348 # While removing it, we only replace \Z if followed by paren, or at |
|
349 # end, to keep from destroying a literal \Z in the pattern. |
|
350 regex_pat = fnmatch.translate(pattern) |
|
351 regex_pat = re.sub(r'\\Z(\(|$)', r'\1', regex_pat) |
|
352 |
|
353 # We want */a/b.py to match on Windows too, so change slash to match |
|
354 # either separator. |
|
355 regex_pat = regex_pat.replace(r"\/", r"[\\/]") |
|
356 # We want case-insensitive matching, so add that flag. |
|
357 regex = re.compile(r"(?i)" + regex_pat) |
|
358 |
370 |
359 # Normalize the result: it must end with a path separator. |
371 # Normalize the result: it must end with a path separator. |
360 result_sep = sep(result) |
372 result_sep = sep(result) |
361 result = result.rstrip(r"\/") + result_sep |
373 result = result.rstrip(r"\/") + result_sep |
362 self.aliases.append((regex, result)) |
374 self.aliases.append((regex, result)) |