""" Wild Card Match. A custom implementation of `glob`. Licensed under MIT Copyright (c) 2018 - 2020 Isaac Muse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import os import sys import re import functools from collections import namedtuple import bracex from . import _wcparse from . import _wcmatch from . import util from typing import ( Optional, Iterator, Iterable, List, AnyStr, Union, Generic, Tuple, Pattern, Callable, Any, Set, Sequence, cast ) __all__ = ( "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH", "EXTGLOB", "EXTMATCH", "GLOBSTAR", "NEGATE", "MINUSNEGATE", "BRACE", "NOUNIQUE", "REALPATH", "FOLLOW", "MATCHBASE", "MARK", "NEGATEALL", "NODIR", "FORCEWIN", "FORCEUNIX", "GLOBTILDE", "NODOTDIR", "SCANDOTDIR", "SUPPORT_DIR_FD", "C", "I", "R", "D", "E", "G", "N", "M", "B", "P", "L", "S", "X", 'K', "O", "A", "W", "U", "T", "Q", "Z", "SD", "iglob", "glob", "globmatch", "globfilter", "escape", "raw_escape", "is_magic" ) # We don't use `util.platform` only because we mock it in tests, # and `scandir` will not work with bytes on the wrong system. WIN = sys.platform.startswith('win') SUPPORT_DIR_FD = _wcmatch.SUPPORT_DIR_FD C = CASE = _wcparse.CASE I = IGNORECASE = _wcparse.IGNORECASE R = RAWCHARS = _wcparse.RAWCHARS D = DOTGLOB = DOTMATCH = _wcparse.DOTMATCH E = EXTGLOB = EXTMATCH = _wcparse.EXTMATCH G = GLOBSTAR = _wcparse.GLOBSTAR N = NEGATE = _wcparse.NEGATE M = MINUSNEGATE = _wcparse.MINUSNEGATE B = BRACE = _wcparse.BRACE P = REALPATH = _wcparse.REALPATH L = FOLLOW = _wcparse.FOLLOW S = SPLIT = _wcparse.SPLIT X = MATCHBASE = _wcparse.MATCHBASE O = NODIR = _wcparse.NODIR A = NEGATEALL = _wcparse.NEGATEALL W = FORCEWIN = _wcparse.FORCEWIN U = FORCEUNIX = _wcparse.FORCEUNIX T = GLOBTILDE = _wcparse.GLOBTILDE Q = NOUNIQUE = _wcparse.NOUNIQUE Z = NODOTDIR = _wcparse.NODOTDIR K = MARK = 0x1000000 SD = SCANDOTDIR = 0x2000000 _PATHLIB = 0x8000000 # Internal flags _EXTMATCHBASE = _wcparse._EXTMATCHBASE _RTL = _wcparse._RTL _NOABSOLUTE = _wcparse._NOABSOLUTE _PATHNAME = _wcparse.PATHNAME FLAG_MASK = ( CASE | IGNORECASE | RAWCHARS | DOTMATCH | EXTMATCH | GLOBSTAR | NEGATE | MINUSNEGATE | BRACE | REALPATH | FOLLOW | SPLIT | MATCHBASE | NODIR | NEGATEALL | FORCEWIN | FORCEUNIX | GLOBTILDE | NOUNIQUE | NODOTDIR | _EXTMATCHBASE | _RTL | _NOABSOLUTE ) _RE_PATHLIB_DOT_NORM = ( re.compile(r'(?:((?<=^)|(?<=/))\.(?:/|$))+'), re.compile(br'(?:((?<=^)|(?<=/))\.(?:/|$))+') ) # type: Tuple[Pattern[str], Pattern[bytes]] _RE_WIN_PATHLIB_DOT_NORM = ( re.compile(r'(?:((?<=^)|(?<=[\\/]))\.(?:[\\/]|$))+'), re.compile(br'(?:((?<=^)|(?<=[\\/]))\.(?:[\\/]|$))+') ) # type: Tuple[Pattern[str], Pattern[bytes]] def _flag_transform(flags: int) -> int: """Transform flags to glob defaults.""" # Enabling both cancels out if flags & FORCEUNIX and flags & FORCEWIN: flags ^= FORCEWIN | FORCEUNIX # Here we force `PATHNAME`. flags = (flags & FLAG_MASK) | _PATHNAME if flags & REALPATH: if util.platform() == "windows": if flags & FORCEUNIX: flags ^= FORCEUNIX flags |= FORCEWIN else: if flags & FORCEWIN: flags ^= FORCEWIN return flags class _GlobPart( namedtuple('_GlobPart', ['pattern', 'is_magic', 'is_globstar', 'dir_only', 'is_drive']), ): """File Glob.""" class _GlobSplit(Generic[AnyStr]): """ Split glob pattern on "magic" file and directories. Glob pattern return a list of patterns broken down at the directory boundary. Each piece will either be a literal file part or a magic part. Each part will will contain info regarding whether they are a directory pattern or a file pattern and whether the part is "magic", etc.: `["pattern", is_magic, is_globstar, dir_only, is_drive]`. Example: `"**/this/is_literal/*magic?/@(magic|part)"` Would become: ``` [ ["**", True, True, False, False], ["this", False, False, True, False], ["is_literal", False, False, True, False], ["*magic?", True, False, True, False], ["@(magic|part)", True, False, False, False] ] ``` """ def __init__(self, pattern: AnyStr, flags: int) -> None: """Initialize.""" self.pattern = pattern # type: AnyStr self.unix = _wcparse.is_unix_style(flags) self.flags = flags self.no_abs = bool(flags & _wcparse._NOABSOLUTE) self.globstar = bool(flags & GLOBSTAR) self.matchbase = bool(flags & MATCHBASE) self.extmatchbase = bool(flags & _wcparse._EXTMATCHBASE) self.tilde = bool(flags & GLOBTILDE) if _wcparse.is_negative(self.pattern, flags): # pragma: no cover # This isn't really used, but we'll keep it around # in case we find a reason to directly send inverse patterns # Through here. self.pattern = self.pattern[0:1] if flags & NEGATE: flags ^= NEGATE self.flags = flags self.extend = bool(flags & EXTMATCH) if not self.unix: self.win_drive_detect = True self.bslash_abort = True self.sep = '\\' else: self.win_drive_detect = False self.bslash_abort = False self.sep = '/' # Once split, Windows file names will never have `\\` in them, # so we can use the Unix magic detect self.magic_symbols = _wcparse._get_magic_symbols(pattern, self.unix, self.flags)[0] # type: Set[AnyStr] def is_magic(self, name: AnyStr) -> bool: """Check if name contains magic characters.""" for c in self.magic_symbols: if c in name: return True return False def _sequence(self, i: util.StringIter) -> None: """Handle character group.""" c = next(i) if c == '!': c = next(i) if c in ('^', '-', '['): c = next(i) while c != ']': if c == '\\': # Handle escapes try: self._references(i, True) except _wcparse.PathNameException: raise StopIteration elif c == '/': raise StopIteration c = next(i) def _references(self, i: util.StringIter, sequence: bool = False) -> str: """Handle references.""" value = '' c = next(i) if c == '\\': # \\ if sequence and self.bslash_abort: raise _wcparse.PathNameException value = c elif c == '/': # \/ if sequence: raise _wcparse.PathNameException value = c else: # \a, \b, \c, etc. pass return value def parse_extend(self, c: str, i: util.StringIter) -> bool: """Parse extended pattern lists.""" # Start list parsing success = True index = i.index list_type = c try: c = next(i) if c != '(': raise StopIteration while c != ')': c = next(i) if self.extend and c in _wcparse.EXT_TYPES and self.parse_extend(c, i): continue if c == '\\': try: self._references(i) except StopIteration: pass elif c == '[': index = i.index try: self._sequence(i) except StopIteration: i.rewind(i.index - index) except StopIteration: success = False c = list_type i.rewind(i.index - index) return success def store(self, value: AnyStr, l: List[_GlobPart], dir_only: bool) -> None: """Group patterns by literals and potential magic patterns.""" if l and value in (b'', ''): return globstar = value in (b'**', '**') and self.globstar magic = self.is_magic(value) if magic: v = cast(Pattern[AnyStr], _wcparse._compile(value, self.flags)) # type: Union[Pattern[AnyStr], AnyStr] else: v = value if globstar and l and l[-1].is_globstar: l[-1] = _GlobPart(v, magic, globstar, dir_only, False) else: l.append(_GlobPart(v, magic, globstar, dir_only, False)) def split(self) -> List[_GlobPart]: """Start parsing the pattern.""" split_index = [] parts = [] start = -1 if isinstance(self.pattern, bytes): is_bytes = True pattern = self.pattern.decode('latin-1') else: is_bytes = False pattern = self.pattern i = util.StringIter(pattern) # Detect and store away windows drive as a literal if self.win_drive_detect: root_specified, drive, slash, end = _wcparse._get_win_drive(pattern) if drive is not None: parts.append(_GlobPart(drive.encode('latin-1') if is_bytes else drive, False, False, True, True)) start = end - 1 i.advance(start) elif drive is None and root_specified: parts.append(_GlobPart(b'\\' if is_bytes else '\\', False, False, True, True)) start = 1 i.advance(2) elif not self.win_drive_detect and pattern.startswith('/'): parts.append(_GlobPart(b'/' if is_bytes else '/', False, False, True, True)) start = 0 i.advance(1) for c in i: if self.extend and c in _wcparse.EXT_TYPES and self.parse_extend(c, i): continue if c == '\\': index = i.index value = '' try: value = self._references(i) if (self.bslash_abort and value == '\\') or value == '/': split_index.append((i.index - 2, 1)) except StopIteration: i.rewind(i.index - index) elif c == '/': split_index.append((i.index - 1, 0)) elif c == '[': index = i.index try: self._sequence(i) except StopIteration: i.rewind(i.index - index) for split, offset in split_index: value = pattern[start + 1:split] self.store(cast(AnyStr, value.encode('latin-1') if is_bytes else value), parts, True) start = split + offset if start < len(pattern): value = pattern[start + 1:] if value: self.store(cast(AnyStr, value.encode('latin-1') if is_bytes else value), parts, False) if len(pattern) == 0: parts.append(_GlobPart(pattern.encode('latin-1') if is_bytes else pattern, False, False, False, False)) if ( (self.extmatchbase and not parts[0].is_drive) or (self.matchbase and len(parts) == 1 and not parts[0].dir_only) ): self.globstar = True parts.insert(0, _GlobPart(b'**' if is_bytes else '**', True, True, True, False)) if self.no_abs and parts and parts[0].is_drive: raise ValueError('The pattern must be a relative path pattern') return parts class Glob(Generic[AnyStr]): """Glob patterns.""" def __init__( self, pattern: Union[str, bytes, Sequence[AnyStr]], flags: int = 0, root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None, dir_fd: Optional[int] = None, limit: int = _wcparse.PATTERN_LIMIT ) -> None: """Initialize the directory walker object.""" pats = _wcparse.to_str_sequence(pattern) self.pattern = [] # type: List[List[_GlobPart]] self.npatterns = [] # type: List[Pattern[AnyStr]] self.seen = set() # type: Set[AnyStr] self.dir_fd = dir_fd if SUPPORT_DIR_FD else None # type: Optional[int] self.nounique = bool(flags & NOUNIQUE) # type: bool self.mark = bool(flags & MARK) # type: bool # Only scan for `.` and `..` if it is specifically requested. self.scandotdir = bool(flags & SCANDOTDIR) # type: bool if self.mark: flags ^= MARK self.negateall = bool(flags & NEGATEALL) # type: bool if self.negateall: flags ^= NEGATEALL self.nodir = bool(flags & NODIR) # type: bool if self.nodir: flags ^= NODIR self.pathlib = bool(flags & _PATHLIB) # type: bool if self.pathlib: flags ^= _PATHLIB # Right to left searching is only for matching if flags & _RTL: # pragma: no cover flags ^= _RTL self.flags = _flag_transform(flags | REALPATH) # type: int self.negate_flags = self.flags # type: int if not self.scandotdir and not self.flags & NODOTDIR: self.flags |= NODOTDIR self.raw_chars = bool(self.flags & RAWCHARS) # type: bool self.follow_links = bool(self.flags & FOLLOW) # type: bool self.dot = bool(self.flags & DOTMATCH) # type: bool self.unix = not bool(self.flags & FORCEWIN) # type: bool self.negate = bool(self.flags & NEGATE) # type: bool self.globstar = bool(self.flags & GLOBSTAR) # type: bool self.braces = bool(self.flags & BRACE) # type: bool self.matchbase = bool(self.flags & MATCHBASE) # type: bool self.case_sensitive = _wcparse.get_case(self.flags) # type: bool self.limit = limit # type: int forcewin = self.flags & FORCEWIN if isinstance(pats[0], bytes): ptype = util.BYTES self.current = b'.' # type: AnyStr self.specials = (b'.', b'..') # type: Tuple[AnyStr, ...] self.empty = b'' # type: AnyStr self.stars = b'**' # type: AnyStr self.sep = b'\\' if forcewin else b'/' # type: AnyStr self.seps = (b'/', self.sep) if forcewin else (self.sep,) # type: Tuple[AnyStr, ...] self.re_pathlib_norm = cast(Pattern[AnyStr], _RE_WIN_PATHLIB_DOT_NORM[ptype]) # type: Pattern[AnyStr] self.re_no_dir = cast(Pattern[AnyStr], _wcparse.RE_WIN_NO_DIR[ptype]) # type: Pattern[AnyStr] else: ptype = util.UNICODE self.current = '.' self.specials = ('.', '..') self.empty = '' self.stars = '**' self.sep = '\\' if forcewin else '/' self.seps = ('/', self.sep) if forcewin else (self.sep,) self.re_pathlib_norm = cast(Pattern[AnyStr], _RE_WIN_PATHLIB_DOT_NORM[ptype]) self.re_no_dir = cast(Pattern[AnyStr], _wcparse.RE_WIN_NO_DIR[ptype]) temp = os.fspath(root_dir) if root_dir is not None else self.current if not isinstance(temp, type(pats[0])): raise TypeError( 'Pattern and root_dir should be of the same type, not {} and {}'.format( type(pats[0]), type(temp) ) ) self.root_dir = temp # type: AnyStr self._parse_patterns(pats) def _iter_patterns(self, patterns: Sequence[AnyStr]) -> Iterator[Tuple[bool, AnyStr]]: """Iterate expanded patterns.""" seen = set() try: current_limit = self.limit total = 0 for p in patterns: p = util.norm_pattern(p, not self.unix, self.raw_chars) count = 0 for count, expanded in enumerate(_wcparse.expand(p, self.flags, current_limit), 1): total += 1 if 0 < self.limit < total: raise _wcparse.PatternLimitException( "Pattern limit exceeded the limit of {:d}".format(self.limit) ) # Filter out duplicate patterns. If `NOUNIQUE` is enabled, # we only want to filter on negative patterns as they are # only filters. is_neg = _wcparse.is_negative(expanded, self.flags) if not self.nounique or is_neg: if expanded in seen: continue seen.add(expanded) yield is_neg, expanded if self.limit: current_limit -= count if current_limit < 1: current_limit = 1 except bracex.ExpansionLimitException: raise _wcparse.PatternLimitException( "Pattern limit exceeded the limit of {:d}".format(self.limit) ) def _parse_patterns(self, patterns: Sequence[AnyStr]) -> None: """Parse patterns.""" for is_neg, p in self._iter_patterns(patterns): if is_neg: # Treat the inverse pattern as a normal pattern if it matches, we will exclude. # This is faster as compiled patterns usually compare the include patterns first, # and then the exclude, but glob will already know it wants to include the file. self.npatterns.append(cast(Pattern[AnyStr], _wcparse._compile(p, self.negate_flags))) else: self.pattern.append(_GlobSplit(p, self.flags).split()) if not self.pattern and self.npatterns: if self.negateall: default = self.stars self.pattern.append(_GlobSplit(default, self.flags | GLOBSTAR).split()) if self.nodir: self.npatterns.append(self.re_no_dir) # A single positive pattern will not find multiples of the same file # disable unique mode so that we won't waste time or memory computing unique returns. if ( len(self.pattern) <= 1 and not self.flags & NODOTDIR and not self.nounique and not (self.pathlib and self.scandotdir) ): self.nounique = True def _is_hidden(self, name: AnyStr) -> bool: """Check if is file hidden.""" return not self.dot and name[0:1] == self.specials[0] def _is_this(self, name: AnyStr) -> bool: """Check if "this" directory `.`.""" return name == self.specials[0] or name == self.sep def _is_parent(self, name: AnyStr) -> bool: """Check if `..`.""" return name == self.specials[1] def _match_excluded(self, filename: AnyStr, is_dir: bool) -> bool: """Check if file should be excluded.""" if is_dir and not filename.endswith(self.sep): filename += self.sep matched = False for pattern in self.npatterns: if pattern.fullmatch(filename): matched = True break return matched def _is_excluded(self, path: AnyStr, is_dir: bool) -> bool: """Check if file is excluded.""" return bool(self.npatterns and self._match_excluded(path, is_dir)) def _match_literal(self, a: AnyStr, b: Optional[AnyStr] = None) -> bool: """Match two names.""" return a.lower() == b if not self.case_sensitive else a == b def _get_matcher(self, target: Optional[Union[AnyStr, Pattern[AnyStr]]]) -> Optional[Callable[..., Any]]: """Get deep match.""" if target is None: matcher = None # type: Optional[Callable[..., Any]] elif isinstance(target, (str, bytes)): # Plain text match if not self.case_sensitive: match = target.lower() else: match = target matcher = functools.partial(self._match_literal, b=match) else: # File match pattern matcher = target.match return matcher def _lexists(self, path: AnyStr) -> bool: """Check if file exists.""" if not self.dir_fd: return os.path.lexists(self.prepend_base(path)) try: os.lstat(self.prepend_base(path), dir_fd=self.dir_fd) except (OSError, ValueError): # pragma: no cover return False else: return True def prepend_base(self, path: AnyStr) -> AnyStr: """Join path to base if pattern is not absolute.""" if self.is_abs_pattern: return path else: return os.path.join(self.root_dir, path) def _iter(self, curdir: Optional[AnyStr], dir_only: bool, deep: bool) -> Iterator[Tuple[AnyStr, bool, bool, bool]]: """Iterate the directory.""" try: fd = None # type: Optional[int] if self.is_abs_pattern and curdir: scandir = curdir # type: Union[AnyStr, int] elif self.dir_fd is not None: fd = scandir = os.open( os.path.join(self.root_dir, curdir) if curdir else self.root_dir, _wcmatch.DIR_FLAGS, dir_fd=self.dir_fd ) else: scandir = os.path.join(self.root_dir, curdir) if curdir else self.root_dir # Python will never return . or .., so fake it. for special in self.specials: yield special, True, True, False try: with os.scandir(scandir) as scan: # type: ignore[type-var] for f in scan: try: hidden = self._is_hidden(f.name) # type: ignore[arg-type] is_dir = f.is_dir() if is_dir: is_link = f.is_symlink() else: # We don't care if a file is a link is_link = False if (not dir_only or is_dir): yield f.name, is_dir, hidden, is_link # type: ignore[misc] except OSError: # pragma: no cover pass finally: if fd is not None: os.close(fd) except OSError: # pragma: no cover pass def _glob_dir( self, curdir: AnyStr, matcher: Optional[Callable[..., Any]], dir_only: bool = False, deep: bool = False ) -> Iterator[Tuple[AnyStr, bool]]: """Recursive directory glob.""" files = list(self._iter(curdir, dir_only, deep)) for file, is_dir, hidden, is_link in files: if file in self.specials: if matcher is not None and matcher(file): yield os.path.join(curdir, file), True continue path = os.path.join(curdir, file) follow = not is_link or self.follow_links if (matcher is None and not hidden and (follow or not deep)) or (matcher and matcher(file)): yield path, is_dir if deep and not hidden and is_dir and follow: yield from self._glob_dir(path, matcher, dir_only, deep) def _glob(self, curdir: AnyStr, part: _GlobPart, rest: List[_GlobPart]) -> Iterator[Tuple[AnyStr, bool]]: """ Handle glob flow. There are really only a couple of cases: - File name. - File name pattern (magic). - Directory. - Directory name pattern (magic). - Extra slashes `////`. - `globstar` `**`. """ is_magic = part.is_magic dir_only = part.dir_only target = part.pattern is_globstar = part.is_globstar if is_magic and is_globstar: # Glob star directory `**`. # Acquire the pattern after the `globstars` if available. # If not, mark that the `globstar` is the end. this = rest.pop(0) if rest else None globstar_end = this is None if this: dir_only = this.dir_only target = this.pattern if globstar_end: target = None # We match `**/next` during a deep glob, so what ever comes back, # we will send back through `_glob` with pattern after `next` (`**/next/after`). # So grab `after` if available. this = rest.pop(0) if rest else None # Deep searching is the unique case where we # might feed in a `None` for the next pattern to match. # Deep glob will account for this. matcher = self._get_matcher(target) # If our pattern ends with `curdir/**`, but does not start with `**` it matches zero or more, # so it should return `curdir/`, signifying `curdir` + no match. # If a pattern follows `**/something`, we always get the appropriate # return already, so this isn't needed in that case. # There is one quirk though with Bash, if `curdir` had magic before `**`, Bash # omits the trailing `/`. We don't worry about that. if globstar_end and curdir: yield os.path.join(curdir, self.empty), True # Search for path, is_dir in self._glob_dir(curdir, matcher, dir_only, deep=True): if this: yield from self._glob(path, this, rest[:]) else: yield path, is_dir elif not dir_only: # Files: no need to recursively search at this point as we are done. matcher = self._get_matcher(target) yield from self._glob_dir(curdir, matcher) else: # Directory: search current directory against pattern # and feed the results back through with the next pattern. this = rest.pop(0) if rest else None matcher = self._get_matcher(target) for path, is_dir in self._glob_dir(curdir, matcher, True): if this: yield from self._glob(path, this, rest[:]) else: yield path, is_dir def _get_starting_paths(self, curdir: AnyStr, dir_only: bool) -> List[Tuple[AnyStr, bool]]: """ Get the starting location. For case sensitive paths, we have to "glob" for it first as Python doesn't like for its users to think about case. By scanning for it, we can get the actual casing and then compare. """ if not self.is_abs_pattern and not self._is_parent(curdir) and not self._is_this(curdir): results = [] matcher = self._get_matcher(curdir) files = list(self._iter(None, dir_only, False)) for file, is_dir, hidden, is_link in files: if file not in self.specials and (matcher is None or matcher(file)): results.append((file, is_dir)) else: results = [(curdir, True)] return results def is_unique(self, path: AnyStr) -> bool: """Test if path is unique.""" if self.nounique: return True unique = False if (path.lower() if not self.case_sensitive else path) not in self.seen: self.seen.add(path) unique = True return unique def _pathlib_norm(self, path: AnyStr) -> AnyStr: """Normalize path as `pathlib` does.""" path = self.re_pathlib_norm.sub(self.empty, path) return path[:-1] if len(path) > 1 and path[-1:] in self.seps else path def format_path(self, path: AnyStr, is_dir: bool, dir_only: bool) -> Iterator[AnyStr]: """Format path.""" path = os.path.join(path, self.empty) if dir_only or (self.mark and is_dir) else path if self.is_unique(self._pathlib_norm(path) if self.pathlib else path): yield path def glob(self) -> Iterator[AnyStr]: """Starts off the glob iterator.""" curdir = self.current for pattern in self.pattern: # If the pattern ends with `/` we return the files ending with `/`. dir_only = pattern[-1].dir_only if pattern else False self.is_abs_pattern = pattern[0].is_drive if pattern else False if pattern: if not pattern[0].is_magic: # Path starts with normal plain text # Lets verify the case of the starting directory (if possible) this = pattern[0] curdir = this[0] # Abort if we cannot find the drive, or if current directory is empty if not curdir or (self.is_abs_pattern and not self._lexists(self.prepend_base(curdir))): continue # Make sure case matches, but running case insensitive # on a case sensitive file system may return more than # one starting location. results = self._get_starting_paths(curdir, dir_only) if not results: continue if this.dir_only: # Glob these directories if they exists for start, is_dir in results: rest = pattern[1:] if rest: this = rest.pop(0) for match, is_dir in self._glob(start, this, rest): if not self._is_excluded(match, is_dir): yield from self.format_path(match, is_dir, dir_only) elif not self._is_excluded(start, is_dir): yield from self.format_path(start, is_dir, dir_only) else: # Return the file(s) and finish. for match, is_dir in results: if self._lexists(match) and not self._is_excluded(match, is_dir): yield from self.format_path(match, is_dir, dir_only) else: # Path starts with a magic pattern, let's get globbing rest = pattern[:] this = rest.pop(0) for match, is_dir in self._glob(curdir if not curdir == self.current else self.empty, this, rest): if not self._is_excluded(match, is_dir): yield from self.format_path(match, is_dir, dir_only) def iglob( patterns: Union[str, bytes, Sequence[AnyStr]], *, flags: int = 0, root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None, dir_fd: Optional[int] = None, limit: int = _wcparse.PATTERN_LIMIT ) -> Iterator[AnyStr]: """Glob.""" if not isinstance(patterns, (str, bytes)) and not patterns: return yield from Glob(patterns, flags, root_dir, dir_fd, limit).glob() def glob( patterns: Union[str, bytes, Sequence[AnyStr]], *, flags: int = 0, root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None, dir_fd: Optional[int] = None, limit: int = _wcparse.PATTERN_LIMIT ) -> List[AnyStr]: """Glob.""" return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit)) def translate( patterns: Union[str, bytes, Sequence[AnyStr]], *, flags: int = 0, limit: int = _wcparse.PATTERN_LIMIT ) -> Tuple[List[AnyStr], List[AnyStr]]: """Translate glob pattern.""" flags = _flag_transform(flags) return _wcparse.translate(_wcparse.to_str_sequence(patterns), flags, limit) def globmatch( filename: Union[AnyStr, 'os.PathLike[AnyStr]'], patterns: Union[str, bytes, Sequence[AnyStr]], *, flags: int = 0, root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None, dir_fd: Optional[int] = None, limit: int = _wcparse.PATTERN_LIMIT ) -> bool: """ Check if filename matches pattern. By default case sensitivity is determined by the file system, but if `case_sensitive` is set, respect that instead. """ pats = _wcparse.to_str_sequence(patterns) # Shortcut out if we have no patterns if not pats: return False ptype = type(pats[0]) rdir = os.fspath(root_dir) if root_dir is not None else root_dir flags = _flag_transform(flags) fname = os.fspath(filename) # Ensure types are not mismatched if (rdir is not None and not isinstance(rdir, ptype)): raise TypeError( "Pattern type of '{}' does not match the resolved type of '{}' of the root dir".format(ptype, type(rdir)) ) return bool(_wcparse.compile(pats, flags, limit).match(fname, rdir, dir_fd)) def globfilter( filenames: Iterable[Union[AnyStr, 'os.PathLike[AnyStr]']], patterns: Union[str, bytes, Sequence[AnyStr]], *, flags: int = 0, root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None, dir_fd: Optional[int] = None, limit: int = _wcparse.PATTERN_LIMIT ) -> List[Union[AnyStr, 'os.PathLike[AnyStr]']]: """Filter names using pattern.""" pats = _wcparse.to_str_sequence(patterns) # Shortcut out if we have no patterns if not pats: return [] ptype = type(pats[0]) rdir = os.fspath(root_dir) if root_dir is not None else root_dir # Ensure types are not mismatched if (rdir is not None and not isinstance(rdir, ptype)): raise TypeError( "Pattern type of '{}' does not match the resolved type of '{}' of the root dir".format(ptype, type(rdir)) ) matches = [] # type: List[Union[AnyStr, 'os.PathLike[AnyStr]']] flags = _flag_transform(flags) obj = _wcparse.compile(pats, flags, limit) for filename in filenames: temp = os.fspath(filename) if obj.match(temp, rdir, dir_fd): matches.append(filename) return matches @util.deprecated("This function will be removed in 9.0.") def raw_escape(pattern: AnyStr, unix: Optional[bool] = None, raw_chars: bool = True) -> AnyStr: """Apply raw character transform before applying escape.""" return _wcparse.escape( util.norm_pattern(pattern, False, raw_chars, True), unix=unix, pathname=True, raw=True ) def escape(pattern: AnyStr, unix: Optional[bool] = None) -> AnyStr: """Escape.""" return _wcparse.escape(pattern, unix=unix) def is_magic(pattern: AnyStr, *, flags: int = 0) -> bool: """Check if the pattern is likely to be magic.""" flags = _flag_transform(flags) return _wcparse.is_magic(pattern, flags)