Index: Doc/library/shutil.rst =================================================================== --- Doc/library/shutil.rst (revision 82778) +++ Doc/library/shutil.rst (working copy) @@ -163,6 +163,44 @@ .. versionadded:: 2.3 +.. function:: which_files(file [, mode=os.F_OK | os.X_OK[, path=None[, pathext=None]]]) + + Generate full paths, where the *file* is accesible under *mode* + and is located in the directory passed as a part of the *file* name, + or in any directory on *path* if a base *file* name is passed. + + The *mode* matches an existing executable file by default. + + The *path* defaults to the ``PATH`` environment variable, + or to :const:`os.defpath` if the ``PATH`` variable is not set. + On Windows, a current directory is searched before directories in the ``PATH`` variable, + but not before directories in an explicitly passed *path* string or iterable. + + The *pathext* is used to match files with any of the extensions appended to *file*. + On Windows, it defaults to the ``PATHEXT`` environment variable. + If the ``PATHEXT`` variable is not set, then the default *pathext* value is hardcoded + for different Windows versions, to match the actual search performed on command execution. + On Windows <= 4.x, ie. NT and older, it defaults to '.COM;.EXE;.BAT;.CMD'. + On Windows 5.x, ie. 2k/XP/2003, the extensions '.VBS;.VBE;.JS;.JSE;.WSF;.WSH' are appended, + On Windows >= 6.x, ie. Vista/2008/7, the extension '.MSC' is further appended. + The actual search on command execution may differ under Wine_, + which may use a `different default value`__, that is `not treated specially here`__. + In each directory, the *file* is first searched without any additional extension, + even when a *pathext* string or iterable is explicitly passed. + + .. _Wine: http://www.winehq.org/ + __ http://source.winehq.org/source/programs/cmd/wcmdmain.c#L1019 + __ http://wiki.winehq.org/DeveloperFaq#detect-wine + + .. versionadded:: 3.2 + +.. function:: which(file [, mode=os.F_OK | os.X_OK[, path=None[, pathext=None]]]) + + Return the first full path matched by :func:`which_files`, + or raise :exc:`IOError` (:const:`errno.ENOENT`). + + .. versionadded:: 3.2 + .. exception:: Error This exception collects exceptions that raised during a multi-file operation. For Index: Lib/shutil.py =================================================================== --- Lib/shutil.py (revision 82778) +++ Lib/shutil.py (working copy) @@ -23,8 +23,8 @@ getgrnam = None __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", - "copytree", "move", "rmtree", "Error", "SpecialFileError", - "ExecError", "make_archive", "get_archive_formats", + "copytree", "move", "rmtree", "which", "which_files", "Error", + "SpecialFileError", "ExecError", "make_archive", "get_archive_formats", "register_archive_format", "unregister_archive_format"] class Error(EnvironmentError): @@ -42,6 +42,25 @@ except NameError: WindowsError = None +_windows = sys.platform.startswith('win') + +if _windows: + def _getwinpathext(*winver): + """Return the default PATHEXT value for a particular Windows version. + + On Windows <= 4.x, ie. NT and older, it defaults to '.COM;.EXE;.BAT;.CMD'. + On Windows 5.x, ie. 2k/XP/2003, the extensions '.VBS;.VBE;.JS;.JSE;.WSF;.WSH' are appended, + On Windows >= 6.x, ie. Vista/2008/7, the extension '.MSC' is further appended. + + Availability: Windows + + """ + if not winver: + winver = sys.getwindowsversion() + + return os.pathsep.join('.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC'.split(';')[:( + winver[0] < 5 and 4 or winver[0] < 6 and -1 or None )]) + def copyfileobj(fsrc, fdst, length=16*1024): """copy data from file-like object fsrc to file-like object fdst""" while 1: @@ -292,6 +311,78 @@ copy2(src, real_dst) os.unlink(src) +def which_files(file, mode=os.F_OK | os.X_OK, path=None, pathext=None): + """Generate full paths, where the file*is accesible under mode + and is located in the directory passed as a part of the file name, + or in any directory on path if a base file name is passed. + + The mode matches an existing executable file by default. + + The path defaults to the PATH environment variable, + or to os.defpath if the PATH variable is not set. + On Windows, a current directory is searched before directories in the PATH variable, + but not before directories in an explicitly passed path string or iterable. + + The pathext is used to match files with any of the extensions appended to file. + On Windows, it defaults to the ``PATHEXT`` environment variable. + If the PATHEXT variable is not set, then the default pathext value is hardcoded + for different Windows versions, to match the actual search performed on command execution. + + On Windows <= 4.x, ie. NT and older, it defaults to '.COM;.EXE;.BAT;.CMD'. + On Windows 5.x, ie. 2k/XP/2003, the extensions '.VBS;.VBE;.JS;.JSE;.WSF;.WSH' are appended, + On Windows >= 6.x, ie. Vista/2008/7, the extension '.MSC' is further appended. + The actual search on command execution may differ under Wine, + which may use a different default value, that is not treated specially here. + + In each directory, the file is first searched without any additional extension, + even when a pathext string or iterable is explicitly passed. + + """ + filepath, file = os.path.split(file) + + if filepath: + path = (filepath,) + elif path is None: + path = os.environ.get('PATH', os.defpath).split(os.pathsep) + if _windows and not os.curdir in path: + path.insert(0, os.curdir) # current directory is always searched first on Windows + elif isinstance(path, basestring): + path = path.split(os.pathsep) + + if pathext is None: + pathext = [''] + if _windows: + pathext += (os.environ.get('PATHEXT', '') or _getwinpathext()).lower().split(os.pathsep) + elif isinstance(pathext, basestring): + pathext = pathext.split(os.pathsep) + + if not '' in pathext: + pathext.insert(0, '') # always check command without extension, even for an explicitly passed pathext + + seen = set() + for dir in path: + if dir: # only non-empty directories are searched + id = os.path.normcase(os.path.abspath(dir)) + if not id in seen: # each directory is searched only once + seen.add(id) + woex = os.path.join(dir, file) + for ext in pathext: + name = woex + ext + if os.path.exists(name) and os.access(name, mode): + yield name + +def which(file, mode=os.F_OK | os.X_OK, path=None, pathext=None): + """Return the first full path matched by which_files(), or raise IOError(errno.ENOENT). + """ + try: + return iter(which_files(file, mode, path, pathext)).next() + except StopIteration: + try: + from errno import ENOENT + except ImportError: + ENOENT = 2 + raise IOError(ENOENT, '%s not found' % (mode & os.X_OK and 'command' or 'file'), file) + def _destinsrc(src, dst): src = abspath(src) dst = abspath(dst) Index: Lib/test/test_shutil.py =================================================================== --- Lib/test/test_shutil.py (revision 82778) +++ Lib/test/test_shutil.py (working copy) @@ -558,7 +558,88 @@ formats = [name for name, params in get_archive_formats()] self.assertNotIn('xxx', formats) + def test_getwindefpath(self): + if shutil._windows: + self.check_getwinpathext('com exe bat cmd', 3) + self.check_getwinpathext('com exe bat cmd', 4) + self.check_getwinpathext('com exe bat cmd vbs vbe js jse wsf wsh', 5) + self.check_getwinpathext('com exe bat cmd vbs vbe js jse wsf wsh msc', 6) + self.check_getwinpathext('com exe bat cmd vbs vbe js jse wsf wsh msc', 7) + def check_getwinpathext(self, extensions, *winver): + result = shutil._getwinpathext(*winver) + expected = os.pathsep.join(['.%s' % ext.upper() for ext in extensions.split()]) + self.assertEqual(result, expected) + + def test_which(self): + + dir = self.mkdtemp() + ext = '.ext' + tmp = tempfile.NamedTemporaryFile(prefix='command-', suffix=ext, dir=dir) + name = tmp.name + file = os.path.basename(name) + here = os.path.join(os.curdir, file) + nonexistent = '%s-nonexistent' % name + path = os.pathsep.join([ nonexistent, name, dir, dir ]) + # Test also that duplicates are removed, and non-existent objects + # or non-directories in path do not trigger any exceptions. + + ### Test permissions + self.check_which(shutil._windows and [name] or [], file, path=path) + self.check_which(shutil._windows and [name] or [], file, mode=os.X_OK, path=path) + # executable flag is not needed on Windows + + self.check_which([name], file, mode=os.F_OK, path=path) + self.check_which([name], file, mode=os.R_OK, path=path) + self.check_which([name], file, mode=os.W_OK, path=path) + self.check_which([name], file, mode=os.R_OK|os.W_OK, path=path) + + os.chmod(name, stat.S_IRWXU) + self.check_which([name], file, mode=os.R_OK|os.W_OK|os.X_OK, path=path) + + ### Test paths + self.check_which([], file, path='') + self.check_which([], file, path=nonexistent) + self.check_which([], nonexistent, path=path) + self.check_which([name], file, path=path) + self.check_which([name], name, path=path) + self.check_which([name], name, path='') + self.check_which([name], name, path=nonexistent) + + cwd = os.getcwd() + os.chdir(dir) + self.check_which([name], file, path=path) + self.check_which([here], file, path=os.curdir) + self.check_which([name], name, path=os.curdir) + self.check_which([], file, path='') + self.check_which([], file, path=nonexistent) + + _save_path = os.environ.get('PATH', '') + os.environ['PATH'] = '' + self.check_which(shutil._windows and [here] or [], file) + os.environ['PATH'] = _save_path + os.chdir(cwd) + + ### Test extensions + self.check_which([], file[:-4], path=path, pathext='') + self.check_which([], file[:-4], path=path, pathext=nonexistent) + self.check_which([name], file[:-4], path=path, pathext=ext) + + self.check_which([name], file, path=path, pathext=ext) + self.check_which([name], file, path=path, pathext='') + self.check_which([name], file, path=path, pathext=nonexistent) + + def check_which(self, expected, *args, **argd): + result = list(shutil.which_files(*args, **argd)) + self.assertEqual(result, expected) + + try: + result = [ shutil.which(*args, **argd) ] + except IOError: + result = [] + self.assertEqual(result[:1], expected[:1]) + + class TestMove(unittest.TestCase): def setUp(self):