Skip to content

Commit

Permalink
Only return PATHEXT matches on win32 if X_OK is in mode
Browse files Browse the repository at this point in the history
  • Loading branch information
csm10495 committed Sep 28, 2023
1 parent e3cdf36 commit 9dd1e2f
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 26 deletions.
20 changes: 11 additions & 9 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1551,17 +1551,19 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
pathext_source = os.getenv("PATHEXT") or _WIN_DEFAULT_PATHEXT
pathext = [ext for ext in pathext_source.split(os.pathsep) if ext]

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

# Attempt to match CMD behavior:
# Only try the given cmd if it has an extension (therefore has a dot)
# or a dot is a pathext in PATHEXT.
# Otherwise use PATHEXT to formulate paths to check.
files = (([cmd] if (dot in cmd or dot in pathext) else []) +
[cmd + ext for ext in pathext])

files = ([cmd] + [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
if mode & os.X_OK and not any(
[os.path.splitext(files[0])[1].upper() == ext.upper() for ext in pathext]
):
files = files[1:]
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 Down
40 changes: 23 additions & 17 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -2323,34 +2323,40 @@ def test_win_path_needs_curdir(self):

# See GH-109590
@unittest.skipUnless(sys.platform == "win32", 'test specific to Windows')
def test_extensionless_file_resolution_no_dot_in_pathext(self):
def test_pathext_enforced_for_execute(self):
with os_helper.EnvironmentVarGuard() as env:
env['PATHEXT'] = ".test;"
env['PATH'] = self.temp_dir if isinstance(self.temp_dir, str) else self.temp_dir.decode()

extensionless_file_in_path = os.path.join(self.temp_dir, self.to_text_type("file"))
open(extensionless_file_in_path, 'w').close()
env["PATH"] = self.temp_dir if isinstance(self.temp_dir, str) else self.temp_dir.decode()
env["PATHEXT"] = ".test"

extensioned_file_in_path = os.path.join(self.temp_dir, self.to_text_type("file.test"))
open(extensioned_file_in_path, 'w').close()
exe = os.path.join(self.temp_dir, self.to_text_type("test.exe"))
open(exe, 'w').close()
os.chmod(exe, 0o755)

# default does not match since .exe is not in PATHEXT
self.assertIsNone(shutil.which(self.to_text_type("test.exe")))

self.assertEqual(shutil.which(self.to_text_type('file'), os.F_OK), extensioned_file_in_path)
# but if we don't use os.X_OK we're ok not matching PATHEXT
self.assertEqual(shutil.which(self.to_text_type("test.exe"), mode=os.F_OK), exe)

# See GH-109590
@unittest.skipUnless(sys.platform == "win32", 'test specific to Windows')
def test_extensionless_file_resolution_dot_in_pathext(self):
def test_pathext_given_extension_preferred(self):
with os_helper.EnvironmentVarGuard() as env:
env['PATHEXT'] = ".test;.;"
env['PATH'] = self.temp_dir if isinstance(self.temp_dir, str) else self.temp_dir.decode()
env["PATH"] = self.temp_dir if isinstance(self.temp_dir, str) else self.temp_dir.decode()
env["PATHEXT"] = ".exe2;.exe"

exe = os.path.join(self.temp_dir, self.to_text_type("test.exe"))
open(exe, 'w').close()
os.chmod(exe, 0o755)

extensionless_file_in_path = os.path.join(self.temp_dir, self.to_text_type("file"))
open(extensionless_file_in_path, 'w').close()
exe2 = os.path.join(self.temp_dir, self.to_text_type("test.exe2"))
open(exe2, 'w').close()
os.chmod(exe2, 0o755)

extensioned_file_in_path = os.path.join(self.temp_dir, self.to_text_type("file.test"))
open(extensioned_file_in_path, 'w').close()
# even though .exe2 is preferred in PATHEXT, we matched directly to test.exe
self.assertEqual(shutil.which(self.to_text_type("test.exe")), exe)

self.assertEqual(shutil.which(self.to_text_type('file')), extensionless_file_in_path)
self.assertEqual(shutil.which(self.to_text_type("test")), exe2)


class TestWhichBytes(TestWhich):
Expand Down

0 comments on commit 9dd1e2f

Please sign in to comment.