mirror of
https://github.com/python/cpython.git
synced 2025-10-09 16:34:44 +00:00
GH-75586: Make shutil.which() on Windows more consistent with the OS (GH-103179)
This commit is contained in:
parent
f184abbdc9
commit
935aa45235
7 changed files with 233 additions and 54 deletions
|
@ -40,6 +40,9 @@ if os.name == 'posix':
|
|||
elif _WINDOWS:
|
||||
import nt
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import _winapi
|
||||
|
||||
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
|
||||
# This should never be removed, see rationale in:
|
||||
# https://bugs.python.org/issue43743#msg393429
|
||||
|
@ -1449,6 +1452,16 @@ def _access_check(fn, mode):
|
|||
and not os.path.isdir(fn))
|
||||
|
||||
|
||||
def _win_path_needs_curdir(cmd, mode):
|
||||
"""
|
||||
On Windows, we can use NeedCurrentDirectoryForExePath to figure out
|
||||
if we should add the cwd to PATH when searching for executables if
|
||||
the mode is executable.
|
||||
"""
|
||||
return (not (mode & os.X_OK)) or _winapi.NeedCurrentDirectoryForExePath(
|
||||
os.fsdecode(cmd))
|
||||
|
||||
|
||||
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
|
||||
|
@ -1459,60 +1472,54 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
|||
path.
|
||||
|
||||
"""
|
||||
# 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
|
||||
return None
|
||||
|
||||
use_bytes = isinstance(cmd, bytes)
|
||||
|
||||
if path is None:
|
||||
path = os.environ.get("PATH", None)
|
||||
if path is None:
|
||||
try:
|
||||
path = os.confstr("CS_PATH")
|
||||
except (AttributeError, ValueError):
|
||||
# os.confstr() or CS_PATH is not available
|
||||
path = os.defpath
|
||||
# bpo-35755: Don't use os.defpath if the PATH environment variable is
|
||||
# set to an empty string
|
||||
|
||||
# PATH='' doesn't match, whereas PATH=':' looks in the current directory
|
||||
if not path:
|
||||
return None
|
||||
|
||||
if use_bytes:
|
||||
path = os.fsencode(path)
|
||||
path = path.split(os.fsencode(os.pathsep))
|
||||
# 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
|
||||
dirname, cmd = os.path.split(cmd)
|
||||
if dirname:
|
||||
path = [dirname]
|
||||
else:
|
||||
path = os.fsdecode(path)
|
||||
path = path.split(os.pathsep)
|
||||
if path is None:
|
||||
path = os.environ.get("PATH", None)
|
||||
if path is None:
|
||||
try:
|
||||
path = os.confstr("CS_PATH")
|
||||
except (AttributeError, ValueError):
|
||||
# os.confstr() or CS_PATH is not available
|
||||
path = os.defpath
|
||||
# bpo-35755: Don't use os.defpath if the PATH environment variable
|
||||
# is set to an empty string
|
||||
|
||||
# PATH='' doesn't match, whereas PATH=':' looks in the current
|
||||
# directory
|
||||
if not path:
|
||||
return None
|
||||
|
||||
if sys.platform == "win32":
|
||||
# The current directory takes precedence on Windows.
|
||||
curdir = os.curdir
|
||||
if use_bytes:
|
||||
curdir = os.fsencode(curdir)
|
||||
if curdir not in path:
|
||||
path = os.fsencode(path)
|
||||
path = path.split(os.fsencode(os.pathsep))
|
||||
else:
|
||||
path = os.fsdecode(path)
|
||||
path = path.split(os.pathsep)
|
||||
|
||||
if sys.platform == "win32" and _win_path_needs_curdir(cmd, mode):
|
||||
curdir = os.curdir
|
||||
if use_bytes:
|
||||
curdir = os.fsencode(curdir)
|
||||
path.insert(0, curdir)
|
||||
|
||||
if sys.platform == "win32":
|
||||
# PATHEXT is necessary to check on Windows.
|
||||
pathext_source = os.getenv("PATHEXT") or _WIN_DEFAULT_PATHEXT
|
||||
pathext = [ext for ext in pathext_source.split(os.pathsep) if ext]
|
||||
|
||||
if use_bytes:
|
||||
pathext = [os.fsencode(ext) for ext in pathext]
|
||||
# See if the given file matches any of the expected path extensions.
|
||||
# This will allow us to short circuit when given "python.exe".
|
||||
# If it does match, only test that one, otherwise we have to try
|
||||
# others.
|
||||
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
|
||||
files = [cmd]
|
||||
else:
|
||||
files = [cmd + ext for ext in pathext]
|
||||
|
||||
# Always try checking the originally given cmd, if it doesn't match, try pathext
|
||||
files = [cmd] + [cmd + ext for ext in pathext]
|
||||
else:
|
||||
# On other platforms you don't have things like PATHEXT to tell you
|
||||
# what file suffixes are executable, so just pass on cmd as-is.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue