Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.12] gh-127001: Fix PATHEXT issues in shutil.which() on Windows (GH-127035) #127158

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -481,12 +481,6 @@ Directory and files operations
or ends with an extension that is in ``PATHEXT``; and filenames that
have no extension can now be found.

.. versionchanged:: 3.12.1
On Windows, if *mode* includes ``os.X_OK``, executables with an
extension in ``PATHEXT`` will be preferred over executables without a
matching extension.
This brings behavior closer to that of Python 3.11.

.. exception:: Error

This exception collects exceptions that are raised during a multi-file
Expand Down
22 changes: 11 additions & 11 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1534,21 +1534,21 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
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]
pathext = pathext_source.split(os.pathsep)
pathext = [ext.rstrip('.') for ext in pathext if ext]

if use_bytes:
pathext = [os.fsencode(ext) for ext in pathext]

files = ([cmd] + [cmd + ext for ext in pathext])
files = [cmd + ext for ext in pathext]

# gh-109590. If we are looking for an executable, we need to look
# for a PATHEXT match. The first cmd is the direct match
# (e.g. python.exe instead of python)
# Check that direct match first if and only if the extension is in PATHEXT
# Otherwise check it last
suffix = os.path.splitext(files[0])[1].upper()
if mode & os.X_OK and not any(suffix == ext.upper() for ext in pathext):
files.append(files.pop(0))
# If X_OK in mode, simulate the cmd.exe behavior: look at direct
# match if and only if the extension is in PATHEXT.
# If X_OK not in mode, simulate the first result of where.exe:
# always look at direct match before a PATHEXT match.
normcmd = cmd.upper()
if not (mode & os.X_OK) or any(normcmd.endswith(ext.upper()) for ext in pathext):
files.insert(0, cmd)
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.
Expand All @@ -1557,7 +1557,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
if normdir not in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
Expand Down
Loading
Loading