# HG changeset patch # User Toby Tobkin # Date 1451539382 28800 # Wed Dec 30 21:23:02 2015 -0800 # Node ID c47866ca8e9cdd66047c1e2b04af9c10dba5eb36 # Parent 2234aea1150c38234c2a563bf4acb1d27740bec3 Second patch for issue #24505, updated to reflect feedback from eryksun diff --git a/Lib/shutil.py b/Lib/shutil.py --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -11,6 +11,8 @@ import collections import errno import tarfile +if os.name == 'nt': + from ctypes import windll try: import bz2 @@ -36,6 +38,8 @@ except ImportError: getgrnam = None +# define implementation here so an OS check is only run once + __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", "copytree", "move", "rmtree", "Error", "SpecialFileError", "ExecError", "make_archive", "get_archive_formats", @@ -1081,51 +1085,34 @@ return os.terminal_size((columns, lines)) -def which(cmd, mode=os.F_OK | os.X_OK, path=None): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. +# The implementation of "which" (called "where" on Windows) is different +# on Windows than most Unix-like systems, thus two different +# implementations are provided, defined at import time. +if os.name == 'nt': + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path + which conforms to the given mode, or None if there is no such + file. - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. + Generally, if a Windows command is not specified as an absolute + or relative path, the current directory and PATH will be + searched. Otherwise, only the specified directory will be + searched. - """ - # Check that a given file can be accessed with the correct mode. - # Additionally check that `file` is not a directory, as on Windows - # directories pass the os.access check. - def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) - and not os.path.isdir(fn)) + The cmd string may or may not include an executable extension + (such as .exe). If not, then all extensions specified by + environmental variable PATHEXT will be tried as valid commands. - # Checks if a given command is an internal Windows NT command such - # as CHDIR Required to properly emulate the Windows command search - # sequence in the event that a requested command also matches an - # internal shell command. See: - # https://technet.microsoft.com/en-us/library/cc723564.aspx - def _is_windows_nt_internal_command(_cmd): - nt_internal_commands = { - 'ASSOC', 'CALL', 'CHDIR', 'CD', 'CLS', 'COLOR', 'COPY', 'DATE', - 'DIR', 'DPATH', 'ECHO', 'ENDLOCAL', 'ERASE', 'DEL', 'EXIT', 'FOR', - 'FTYPE', 'GOTO', 'IF', 'MKDIR', 'MD', 'MOVE', 'PATH', 'PAUSE', - 'POPD', 'PROMPT', 'PUSHD', 'REM', 'RENAME', 'REN', 'RMDIR', 'RD', - 'SET', 'SETLOCAL', 'SHIFT', 'START', 'TIME', 'TITLE', 'TYPE', 'VER' - } - cmd_upper = _cmd.upper() + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the + result of os.environ.get("PATH"), or can be overridden with a + custom search path. - if sys.platform != "win32": - return False - else: - return cmd_upper in nt_internal_commands - - # Implementation of which for supported Windows platforms. Windows - # has different semantics for command execution than *nix platforms - def _which_windows_nt(_cmd, _mode, _path): + """ # Per cmd.exe's specification, NT internal commands not prefaced # by a path separator are executed as a shell command. E.g.: # "CHDIR" will execute a shell command, but ".\\CHDIR" will # search for an executable named "CHDIR" - if not os.path.dirname(_cmd) and _is_windows_nt_internal_command(_cmd): + if not os.path.dirname(cmd) and _is_windows_nt_internal_command(cmd): return None # On Windows, a command such as ".\\python" can match @@ -1134,7 +1121,7 @@ # ".\\python.exe.exe" pathext = [ext.lower() for ext in os.environ.get("PATHEXT", "").split(os.pathsep)] - file_name = os.path.split(_cmd)[1].lower() + file_name = os.path.split(cmd)[1].lower() file_ext = os.path.splitext(file_name)[1].lower() if file_ext == '': # order matters to the Windows shell @@ -1149,20 +1136,27 @@ # If the provided command is a specific path to an executable # (e.g. '.\\python.exe' or 'C:\\bin\\putty.exe', ignore the PATH # env variable - if os.path.dirname(_cmd): - search_paths = [os.path.split(_cmd)[0]] + if os.path.dirname(cmd): + search_paths = [os.path.split(cmd)[0]] if not os.path.isdir(search_paths[0]): return None else: - if _path == None: - search_paths = os.environ.get("PATH", "") + if path is None: + if os.environ.get("PATH", "") != '': + search_paths = os.environ.get("PATH", "").split(os.pathsep) + else: + search_paths = [] else: - search_paths = _path + search_paths = [path] + # Use the 'W' (for "wide") versions of win32 API calls + # because Python uses Unicode strings, not single-byte ASCII + # ones + if windll.kernel32.NeedCurrentDirectoryForExePathW(cmd) != 0: + search_paths = [os.curdir] + search_paths # If we don't return None, the user will get a nonsense # error if os.listdir() is called later - if search_paths == '': + if not search_paths: return None - search_paths = [os.curdir] + search_paths.split(os.pathsep) # order of nesting of the loops matters for cur_path in search_paths: @@ -1171,46 +1165,73 @@ file_path = os.path.join(cur_path, cur_listing) if (cur_potential_match == cur_listing.lower() and os.path.isfile(file_path) and - _access_check(file_path, _mode)): + os.access(file_path, mode)): return file_path return None +else: + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path + which conforms to the given mode on the PATH, or None if there + is no such file. - # Implementation of which for all supported platforms besides - # Windows - def _which(_cmd, _mode, _path): + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the + result of os.environ.get("PATH"), or can be overridden with a + custom search path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on + # Windows directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + # If we're given a path with a directory part, look it up # directly rather than referring to PATH directories. This # includes checking relative to the current directory, e.g. # ./script - if os.path.dirname(_cmd): - if _access_check(_cmd, _mode): - return _cmd + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd return None - if _path is None: - _path = os.environ.get("PATH", os.defpath) - if not _path: + if path is None: + path = os.environ.get("PATH", os.defpath) + if not path: return None - _path = _path.split(os.pathsep) + path = path.split(os.pathsep) - files = [_cmd] + files = [cmd] seen = set() - for dir in _path: + for dir in path: normdir = os.path.normcase(dir) if not normdir in seen: seen.add(normdir) for thefile in files: name = os.path.join(dir, thefile) - if _access_check(name, _mode): + if _access_check(name, mode): return name return None - # The logic for which() on Windows (called where.exe) is separated - # from the logic for which() on other platforms because of the - # substantially different semantics - if os.name == 'nt': - return _which_windows_nt(cmd, mode, path) - else: - return _which(cmd, mode, path) +# Checks if a given command is an internal Windows NT command such +# as CHDIR Required to properly emulate the Windows command search +# sequence in the event that a requested command also matches an +# internal shell command. See: +# https://technet.microsoft.com/en-us/library/cc723564.aspx +if os.name == 'nt': + def _is_windows_nt_internal_command(_cmd): + nt_internal_commands = { + 'ASSOC', 'CALL', 'CHDIR', 'CD', 'CLS', 'COLOR', 'COPY', 'DATE', + 'DIR', 'DPATH', 'ECHO', 'ENDLOCAL', 'ERASE', 'DEL', 'EXIT', 'FOR', + 'FTYPE', 'GOTO', 'IF', 'MKDIR', 'MD', 'MOVE', 'PATH', 'PAUSE', + 'POPD', 'PROMPT', 'PUSHD', 'REM', 'RENAME', 'REN', 'RMDIR', 'RD', + 'SET', 'SETLOCAL', 'SHIFT', 'START', 'TIME', 'TITLE', 'TYPE', 'VER' + } + cmd_upper = _cmd.upper() + + if sys.platform != "win32": + return False + else: + return cmd_upper in nt_internal_commands \ No newline at end of file diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1557,7 +1557,10 @@ support.EnvironmentVarGuard() as env: env['PATH'] = self.dir rv = shutil.which(self.file, path='') - self.assertIsNone(rv) + if os.name == 'nt': + self.assertEqual(rv, os.path.join(os.curdir, self.file)) + else: + self.assertIsNone(rv) def test_empty_path_no_PATH(self): with support.EnvironmentVarGuard() as env: