diff --git a/Lib/glob.py b/Lib/glob.py index 002cd92019..6ad5d7c92f 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -6,7 +6,7 @@ import fnmatch __all__ = ["glob", "iglob", "escape"] -def glob(pathname, *, recursive=False): +def glob(pathname, *, recursive=False, symlinks=True): """Return a list of paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la @@ -16,10 +16,13 @@ def glob(pathname, *, recursive=False): If recursive is true, the pattern '**' will match any files and zero or more directories and subdirectories. + + If symlinks is true (the default), '*' and '**' will match symlinks. + Literal components will always match symlinks. """ - return list(iglob(pathname, recursive=recursive)) + return list(iglob(pathname, recursive=recursive, symlinks=symlinks)) -def iglob(pathname, *, recursive=False): +def iglob(pathname, *, recursive=False, symlinks=True): """Return an iterator which yields the paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la @@ -29,14 +32,20 @@ def iglob(pathname, *, recursive=False): If recursive is true, the pattern '**' will match any files and zero or more directories and subdirectories. + + If symlinks is true (the default), '*' and '**' will match symlinks. + Literal components will always match symlinks. """ - it = _iglob(pathname, recursive, False) + it = _iglob(pathname, recursive, False, symlinks) if recursive and _isrecursive(pathname): s = next(it) # skip empty string assert not s return it -def _iglob(pathname, recursive, dironly): +def _iglob(pathname, recursive, dironly, symlinks): + # TODO: possibly the internal functions should switch to a single 'flags' + # bitfield argument instead of many booleans. The C function glob(3) has + # a lot more options not yet supported in Python. dirname, basename = os.path.split(pathname) if not has_magic(pathname): assert not dironly @@ -50,15 +59,15 @@ def _iglob(pathname, recursive, dironly): return if not dirname: if recursive and _isrecursive(basename): - yield from _glob2(dirname, basename, dironly) + yield from _glob2(dirname, basename, dironly, symlinks) else: - yield from _glob1(dirname, basename, dironly) + yield from _glob1(dirname, basename, dironly, symlinks) return # `os.path.split()` returns the argument itself as a dirname if it is a # drive or UNC path. Prevent an infinite recursion if a drive or UNC path # contains magic characters (i.e. r'\\?\C:'). if dirname != pathname and has_magic(dirname): - dirs = _iglob(dirname, recursive, True) + dirs = _iglob(dirname, recursive, True, symlinks) else: dirs = [dirname] if has_magic(basename): @@ -69,20 +78,21 @@ def _iglob(pathname, recursive, dironly): else: glob_in_dir = _glob0 for dirname in dirs: - for name in glob_in_dir(dirname, basename, dironly): + for name in glob_in_dir(dirname, basename, dironly, symlinks): yield os.path.join(dirname, name) # These 2 helper functions non-recursively glob inside a literal directory. # They return a list of basenames. _glob1 accepts a pattern while _glob0 # takes a literal basename (so it only has to check for its existence). -def _glob1(dirname, pattern, dironly): - names = list(_iterdir(dirname, dironly)) +def _glob1(dirname, pattern, dironly, symlinks): + names = list(_iterdir(dirname, dironly, symlinks)) if not _ishidden(pattern): names = (x for x in names if not _ishidden(x)) return fnmatch.filter(names, pattern) -def _glob0(dirname, basename, dironly): +def _glob0(dirname, basename, dironly, symlinks): + # Ignore 'symlinks' argument - it only applies to '*' and '**'. if not basename: # `os.path.split()` returns an empty basename for paths ending with a # directory separator. 'q*x/' should match only directories. @@ -96,22 +106,22 @@ def _glob0(dirname, basename, dironly): # Following functions are not public but can be used by third-party code. def glob0(dirname, pattern): - return _glob0(dirname, pattern, False) + return _glob0(dirname, pattern, False, True) def glob1(dirname, pattern): - return _glob1(dirname, pattern, False) + return _glob1(dirname, pattern, False, True) # This helper function recursively yields relative pathnames inside a literal # directory. -def _glob2(dirname, pattern, dironly): +def _glob2(dirname, pattern, dironly, symlinks): assert _isrecursive(pattern) yield pattern[:0] - yield from _rlistdir(dirname, dironly) + yield from _rlistdir(dirname, dironly, symlinks) # If dironly is false, yields all file names inside a directory. # If dironly is true, yields only directory names. -def _iterdir(dirname, dironly): +def _iterdir(dirname, dironly, symlinks): if not dirname: if isinstance(dirname, bytes): dirname = bytes(os.curdir, 'ASCII') @@ -121,6 +131,8 @@ def _iterdir(dirname, dironly): with os.scandir(dirname) as it: for entry in it: try: + if not symlinks and entry.is_symlink(): + continue if not dironly or entry.is_dir(): yield entry.name except OSError: @@ -129,13 +141,13 @@ def _iterdir(dirname, dironly): return # Recursively yields relative pathnames inside a literal directory. -def _rlistdir(dirname, dironly): - names = list(_iterdir(dirname, dironly)) +def _rlistdir(dirname, dironly, symlinks): + names = list(_iterdir(dirname, dironly, symlinks)) for x in names: if not _ishidden(x): yield x path = os.path.join(dirname, x) if dirname else x - for y in _rlistdir(path, dironly): + for y in _rlistdir(path, dironly, symlinks): yield os.path.join(x, y) diff --git a/Lib/test/test_glob.py b/Lib/test/test_glob.py index dce64f9fcb..241bd703cc 100644 --- a/Lib/test/test_glob.py +++ b/Lib/test/test_glob.py @@ -161,6 +161,23 @@ class GlobTests(unittest.TestCase): eq(self.glob('sym1'), [self.norm('sym1')]) eq(self.glob('sym2'), [self.norm('sym2')]) + @skip_unless_symlink + def test_glob_disable_symlinks(self): + # Non-magic components still do follow symlinks. + eq = self.assertSequencesEqual_noorder + eq(self.glob('sym3', symlinks=False), [self.norm('sym3')]) + eq(self.glob('sym3', '*', symlinks=False), [self.norm('sym3', 'EF'), + self.norm('sym3', 'efg')]) + self.assertIn(self.glob('sym3' + os.sep, symlinks=False), + [[self.norm('sym3')], [self.norm('sym3') + os.sep]]) + eq(self.glob('*', '*F', symlinks=False), + [self.norm('aaa', 'zzzF'), + self.norm('aab', 'F')]) + + eq(self.glob('sym*', symlinks=False), []) + eq(self.glob('sym1', symlinks=False), [self.norm('sym1')]) + eq(self.glob('sym2', symlinks=False), [self.norm('sym2')]) + @unittest.skipUnless(sys.platform == "win32", "Win32 specific test") def test_glob_magic_in_drive(self): eq = self.assertSequencesEqual_noorder