codekingpro/portable-devtools
114k
1"""2Filename globbing utility. Mostly a copy of `glob` from Python 3.5.3 4Changes include:5 * `yield from` and PEP3102 `*` removed.6 * Hidden files are not ignored.7"""8 9from __future__ import annotations10 11import fnmatch12import os13import re14from collections.abc import Iterable, Iterator15from typing import TYPE_CHECKING, AnyStr, overload16 17if TYPE_CHECKING:18 from _typeshed import BytesPath, StrOrBytesPath, StrPath19 20__all__ = ["glob", "iglob", "escape"]21 22 23def glob(pathname: AnyStr, recursive: bool = False) -> list[AnyStr]:24 """Return a list of paths matching a pathname pattern.25 26 The pattern may contain simple shell-style wildcards a la27 fnmatch. However, unlike fnmatch, filenames starting with a28 dot are special cases that are not matched by '*' and '?'29 patterns.30 31 If recursive is true, the pattern '**' will match any files and32 zero or more directories and subdirectories.33 """34 return list(iglob(pathname, recursive=recursive))35 36 37def iglob(pathname: AnyStr, recursive: bool = False) -> Iterator[AnyStr]:38 """Return an iterator which yields the paths matching a pathname pattern.39 40 The pattern may contain simple shell-style wildcards a la41 fnmatch. However, unlike fnmatch, filenames starting with a42 dot are special cases that are not matched by '*' and '?'43 patterns.44 45 If recursive is true, the pattern '**' will match any files and46 zero or more directories and subdirectories.47 """48 it = _iglob(pathname, recursive)49 if recursive and _isrecursive(pathname):50 s = next(it) # skip empty string51 assert not s52 return it53 54 55def _iglob(pathname: AnyStr, recursive: bool) -> Iterator[AnyStr]:56 dirname, basename = os.path.split(pathname)57 glob_in_dir = glob2 if recursive and _isrecursive(basename) else glob158 59 if not has_magic(pathname):60 if basename:61 if os.path.lexists(pathname):62 yield pathname63 else:64 # Patterns ending with a slash should match only directories65 if os.path.isdir(dirname):66 yield pathname67 return68 69 if not dirname:70 yield from glob_in_dir(dirname, basename)71 return72 # `os.path.split()` returns the argument itself as a dirname if it is a73 # drive or UNC path. Prevent an infinite recursion if a drive or UNC path74 # contains magic characters (i.e. r'\\?\C:').75 if dirname != pathname and has_magic(dirname):76 dirs: Iterable[AnyStr] = _iglob(dirname, recursive)77 else:78 dirs = [dirname]79 if not has_magic(basename):80 glob_in_dir = glob081 for dirname in dirs:82 for name in glob_in_dir(dirname, basename):83 yield os.path.join(dirname, name)84 85 86# These 2 helper functions non-recursively glob inside a literal directory.87# They return a list of basenames. `glob1` accepts a pattern while `glob0`88# takes a literal basename (so it only has to check for its existence).89 90 91@overload92def glob1(dirname: StrPath, pattern: str) -> list[str]: ...93@overload94def glob1(dirname: BytesPath, pattern: bytes) -> list[bytes]: ...95def glob1(dirname: StrOrBytesPath, pattern: str | bytes) -> list[str] | list[bytes]:96 if not dirname:97 if isinstance(pattern, bytes):98 dirname = os.curdir.encode('ASCII')99 else:100 dirname = os.curdir101 try:102 names = os.listdir(dirname)103 except OSError:104 return []105 # mypy false-positives: str or bytes type possibility is always kept in sync106 return fnmatch.filter(names, pattern) # type: ignore[type-var, return-value]107 108 109def glob0(dirname, basename):110 if not basename:111 # `os.path.split()` returns an empty basename for paths ending with a112 # directory separator. 'q*x/' should match only directories.113 if os.path.isdir(dirname):114 return [basename]115 else:116 if os.path.lexists(os.path.join(dirname, basename)):117 return [basename]118 return []119 120 121# This helper function recursively yields relative pathnames inside a literal122# directory.123 124 125@overload126def glob2(dirname: StrPath, pattern: str) -> Iterator[str]: ...127@overload128def glob2(dirname: BytesPath, pattern: bytes) -> Iterator[bytes]: ...129def glob2(dirname: StrOrBytesPath, pattern: str | bytes) -> Iterator[str | bytes]:130 assert _isrecursive(pattern)131 yield pattern[:0]132 yield from _rlistdir(dirname)133 134 135# Recursively yields relative pathnames inside a literal directory.136@overload137def _rlistdir(dirname: StrPath) -> Iterator[str]: ...138@overload139def _rlistdir(dirname: BytesPath) -> Iterator[bytes]: ...140def _rlistdir(dirname: StrOrBytesPath) -> Iterator[str | bytes]:141 if not dirname:142 if isinstance(dirname, bytes):143 dirname = os.curdir.encode('ASCII')144 else:145 dirname = os.curdir146 try:147 names = os.listdir(dirname)148 except OSError:149 return150 for x in names:151 yield x152 # mypy false-positives: str or bytes type possibility is always kept in sync153 path = os.path.join(dirname, x) if dirname else x # type: ignore[arg-type]154 for y in _rlistdir(path):155 yield os.path.join(x, y) # type: ignore[arg-type]156 157 158magic_check = re.compile('([*?[])')159magic_check_bytes = re.compile(b'([*?[])')160 161 162def has_magic(s: str | bytes) -> bool:163 if isinstance(s, bytes):164 return magic_check_bytes.search(s) is not None165 else:166 return magic_check.search(s) is not None167 168 169def _isrecursive(pattern: str | bytes) -> bool:170 if isinstance(pattern, bytes):171 return pattern == b'**'172 else:173 return pattern == '**'174 175 176def escape(pathname):177 """Escape all special characters."""178 # Escaping is done by wrapping any of "*?[" between square brackets.179 # Metacharacters do not work in the drive part and shouldn't be escaped.180 drive, pathname = os.path.splitdrive(pathname)181 if isinstance(pathname, bytes):182 pathname = magic_check_bytes.sub(rb'[\1]', pathname)183 else:184 pathname = magic_check.sub(r'[\1]', pathname)185 return drive + pathname186 