Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -491,12 +491,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 @@ -1550,21 +1550,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('.') and ext for ext in pathext if ext]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really dislike using and for value selection. I trust that you've got it right here, but I can't read it easily. Can we maybe comment?

# Replace ext of '.' with empty string, but keep trailing dots on '.ext.'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think that it is better to remove dots in all cases. The behavior of cmd.exe if PATHEXT contains .BAT. and there is a.bat in the path:

  • a -- hangs
  • a.bat -- found
  • a. -- not found
  • a.bat. -- not found

which() will now return the same result, except for the first case. I think this is the best option for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me


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 @@ -1573,7 +1573,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