Skip to content

Commit 7f22b87

Browse files
[3.13] gh-127001: Fix PATHEXT issues in shutil.which() on Windows (GH-127035) (GH-127156)
* Name without a PATHEXT extension is only searched if the mode does not include X_OK. * Support multi-component PATHEXT extensions (e.g. ".foo.bar"). * Support files without extensions in PATHEXT contains dot-only extension (".", "..", etc). * Support PATHEXT extensions that end with a dot (e.g. ".foo."). (cherry picked from commit 8899e85) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 5468d21 commit 7f22b87

File tree

4 files changed

+303
-248
lines changed

4 files changed

+303
-248
lines changed

Doc/library/shutil.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -491,12 +491,6 @@ Directory and files operations
491491
or ends with an extension that is in ``PATHEXT``; and filenames that
492492
have no extension can now be found.
493493

494-
.. versionchanged:: 3.12.1
495-
On Windows, if *mode* includes ``os.X_OK``, executables with an
496-
extension in ``PATHEXT`` will be preferred over executables without a
497-
matching extension.
498-
This brings behavior closer to that of Python 3.11.
499-
500494
.. exception:: Error
501495

502496
This exception collects exceptions that are raised during a multi-file

Lib/shutil.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,21 +1551,21 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
15511551
if sys.platform == "win32":
15521552
# PATHEXT is necessary to check on Windows.
15531553
pathext_source = os.getenv("PATHEXT") or _WIN_DEFAULT_PATHEXT
1554-
pathext = [ext for ext in pathext_source.split(os.pathsep) if ext]
1554+
pathext = pathext_source.split(os.pathsep)
1555+
pathext = [ext.rstrip('.') for ext in pathext if ext]
15551556

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

1559-
files = ([cmd] + [cmd + ext for ext in pathext])
1560+
files = [cmd + ext for ext in pathext]
15601561

1561-
# gh-109590. If we are looking for an executable, we need to look
1562-
# for a PATHEXT match. The first cmd is the direct match
1563-
# (e.g. python.exe instead of python)
1564-
# Check that direct match first if and only if the extension is in PATHEXT
1565-
# Otherwise check it last
1566-
suffix = os.path.splitext(files[0])[1].upper()
1567-
if mode & os.X_OK and not any(suffix == ext.upper() for ext in pathext):
1568-
files.append(files.pop(0))
1562+
# If X_OK in mode, simulate the cmd.exe behavior: look at direct
1563+
# match if and only if the extension is in PATHEXT.
1564+
# If X_OK not in mode, simulate the first result of where.exe:
1565+
# always look at direct match before a PATHEXT match.
1566+
normcmd = cmd.upper()
1567+
if not (mode & os.X_OK) or any(normcmd.endswith(ext.upper()) for ext in pathext):
1568+
files.insert(0, cmd)
15691569
else:
15701570
# On other platforms you don't have things like PATHEXT to tell you
15711571
# what file suffixes are executable, so just pass on cmd as-is.
@@ -1574,7 +1574,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
15741574
seen = set()
15751575
for dir in path:
15761576
normdir = os.path.normcase(dir)
1577-
if not normdir in seen:
1577+
if normdir not in seen:
15781578
seen.add(normdir)
15791579
for thefile in files:
15801580
name = os.path.join(dir, thefile)

0 commit comments

Comments
 (0)