Skip to content

Commit 394b991

Browse files
authored
[3.7] bpo-35755: shutil.which() uses os.confstr("CS_PATH") (GH-12862)
* bpo-35755: shutil.which() uses os.confstr("CS_PATH") (GH-12858) shutil.which() and distutils.spawn.find_executable() now use os.confstr("CS_PATH") if available instead of os.defpath, if the PATH environment variable is not set. Don't use os.confstr("CS_PATH") nor os.defpath if the PATH environment variable is set to an empty string. Changes: * find_executable() now starts by checking for the executable in the current working directly case. Add an explicit "if not path: return None". * Add tests for PATH='' (empty string), PATH=':' and for PATHEXT. (cherry picked from commit 228a3c9) * bpo-35755: Remove current directory from posixpath.defpath (GH-11586) Document the change in a NEWS entry of the Security category. (cherry picked from commit 2c4c02f)
1 parent b87a807 commit 394b991

7 files changed

Lines changed: 159 additions & 19 deletions

File tree

Lib/distutils/spawn.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,32 @@ def find_executable(executable, path=None):
172172
A string listing directories separated by 'os.pathsep'; defaults to
173173
os.environ['PATH']. Returns the complete filename or None if not found.
174174
"""
175-
if path is None:
176-
path = os.environ.get('PATH', os.defpath)
177-
178-
paths = path.split(os.pathsep)
179-
base, ext = os.path.splitext(executable)
180-
175+
_, ext = os.path.splitext(executable)
181176
if (sys.platform == 'win32') and (ext != '.exe'):
182177
executable = executable + '.exe'
183178

184-
if not os.path.isfile(executable):
185-
for p in paths:
186-
f = os.path.join(p, executable)
187-
if os.path.isfile(f):
188-
# the file exists, we have a shot at spawn working
189-
return f
190-
return None
191-
else:
179+
if os.path.isfile(executable):
192180
return executable
181+
182+
if path is None:
183+
path = os.environ.get('PATH', None)
184+
if path is None:
185+
try:
186+
path = os.confstr("CS_PATH")
187+
except (AttributeError, ValueError):
188+
# os.confstr() or CS_PATH is not available
189+
path = os.defpath
190+
# bpo-35755: Don't use os.defpath if the PATH environment variable is
191+
# set to an empty string
192+
193+
# PATH='' doesn't match, whereas PATH=':' looks in the current directory
194+
if not path:
195+
return None
196+
197+
paths = path.split(os.pathsep)
198+
for p in paths:
199+
f = os.path.join(p, executable)
200+
if os.path.isfile(f):
201+
# the file exists, we have a shot at spawn working
202+
return f
203+
return None

Lib/distutils/tests/test_spawn.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,52 @@ def test_find_executable(self):
8787
rv = find_executable(dont_exist_program , path=tmp_dir)
8888
self.assertIsNone(rv)
8989

90-
# test os.defpath: missing PATH environment variable
90+
# PATH='': no match, except in the current directory
9191
with test_support.EnvironmentVarGuard() as env:
92-
with mock.patch('distutils.spawn.os.defpath', tmp_dir):
93-
env.pop('PATH')
92+
env['PATH'] = ''
93+
with unittest.mock.patch('distutils.spawn.os.confstr',
94+
return_value=tmp_dir, create=True), \
95+
unittest.mock.patch('distutils.spawn.os.defpath',
96+
tmp_dir):
97+
rv = find_executable(program)
98+
self.assertIsNone(rv)
99+
100+
# look in current directory
101+
with test_support.change_cwd(tmp_dir):
102+
rv = find_executable(program)
103+
self.assertEqual(rv, program)
104+
105+
# PATH=':': explicitly looks in the current directory
106+
with test_support.EnvironmentVarGuard() as env:
107+
env['PATH'] = os.pathsep
108+
with unittest.mock.patch('distutils.spawn.os.confstr',
109+
return_value='', create=True), \
110+
unittest.mock.patch('distutils.spawn.os.defpath', ''):
111+
rv = find_executable(program)
112+
self.assertIsNone(rv)
113+
114+
# look in current directory
115+
with test_support.change_cwd(tmp_dir):
116+
rv = find_executable(program)
117+
self.assertEqual(rv, program)
118+
119+
# missing PATH: test os.confstr("CS_PATH") and os.defpath
120+
with test_support.EnvironmentVarGuard() as env:
121+
env.pop('PATH', None)
122+
123+
# without confstr
124+
with unittest.mock.patch('distutils.spawn.os.confstr',
125+
side_effect=ValueError,
126+
create=True), \
127+
unittest.mock.patch('distutils.spawn.os.defpath',
128+
tmp_dir):
129+
rv = find_executable(program)
130+
self.assertEqual(rv, filename)
94131

132+
# with confstr
133+
with unittest.mock.patch('distutils.spawn.os.confstr',
134+
return_value=tmp_dir, create=True), \
135+
unittest.mock.patch('distutils.spawn.os.defpath', ''):
95136
rv = find_executable(program)
96137
self.assertEqual(rv, filename)
97138

Lib/posixpath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
extsep = '.'
1919
sep = '/'
2020
pathsep = ':'
21-
defpath = ':/bin:/usr/bin'
21+
defpath = '/bin:/usr/bin'
2222
altsep = None
2323
devnull = '/dev/null'
2424

Lib/shutil.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,17 @@ def _access_check(fn, mode):
11381138
return None
11391139

11401140
if path is None:
1141-
path = os.environ.get("PATH", os.defpath)
1141+
path = os.environ.get("PATH", None)
1142+
if path is None:
1143+
try:
1144+
path = os.confstr("CS_PATH")
1145+
except (AttributeError, ValueError):
1146+
# os.confstr() or CS_PATH is not available
1147+
path = os.defpath
1148+
# bpo-35755: Don't use os.defpath if the PATH environment variable is
1149+
# set to an empty string
1150+
1151+
# PATH='' doesn't match, whereas PATH=':' looks in the current directory
11421152
if not path:
11431153
return None
11441154
path = path.split(os.pathsep)

Lib/test/test_shutil.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,57 @@ def test_environ_path(self):
15001500
rv = shutil.which(self.file)
15011501
self.assertEqual(rv, self.temp_file.name)
15021502

1503+
def test_environ_path_empty(self):
1504+
# PATH='': no match
1505+
with support.EnvironmentVarGuard() as env:
1506+
env['PATH'] = ''
1507+
with unittest.mock.patch('os.confstr', return_value=self.dir, \
1508+
create=True), \
1509+
support.swap_attr(os, 'defpath', self.dir), \
1510+
support.change_cwd(self.dir):
1511+
rv = shutil.which(self.file)
1512+
self.assertIsNone(rv)
1513+
1514+
def test_environ_path_cwd(self):
1515+
expected_cwd = os.path.basename(self.temp_file.name)
1516+
if sys.platform == "win32":
1517+
curdir = os.curdir
1518+
if isinstance(expected_cwd, bytes):
1519+
curdir = os.fsencode(curdir)
1520+
expected_cwd = os.path.join(curdir, expected_cwd)
1521+
1522+
# PATH=':': explicitly looks in the current directory
1523+
with support.EnvironmentVarGuard() as env:
1524+
env['PATH'] = os.pathsep
1525+
with unittest.mock.patch('os.confstr', return_value=self.dir, \
1526+
create=True), \
1527+
support.swap_attr(os, 'defpath', self.dir):
1528+
rv = shutil.which(self.file)
1529+
self.assertIsNone(rv)
1530+
1531+
# look in current directory
1532+
with support.change_cwd(self.dir):
1533+
rv = shutil.which(self.file)
1534+
self.assertEqual(rv, expected_cwd)
1535+
1536+
def test_environ_path_missing(self):
1537+
with support.EnvironmentVarGuard() as env:
1538+
env.pop('PATH', None)
1539+
1540+
# without confstr
1541+
with unittest.mock.patch('os.confstr', side_effect=ValueError, \
1542+
create=True), \
1543+
support.swap_attr(os, 'defpath', self.dir):
1544+
rv = shutil.which(self.file)
1545+
self.assertEqual(rv, self.temp_file.name)
1546+
1547+
# with confstr
1548+
with unittest.mock.patch('os.confstr', return_value=self.dir, \
1549+
create=True), \
1550+
support.swap_attr(os, 'defpath', ''):
1551+
rv = shutil.which(self.file)
1552+
self.assertEqual(rv, self.temp_file.name)
1553+
15031554
def test_empty_path(self):
15041555
base_dir = os.path.dirname(self.dir)
15051556
with support.change_cwd(path=self.dir), \
@@ -1514,6 +1565,23 @@ def test_empty_path_no_PATH(self):
15141565
rv = shutil.which(self.file)
15151566
self.assertIsNone(rv)
15161567

1568+
@unittest.skipUnless(sys.platform == "win32", 'test specific to Windows')
1569+
def test_pathext(self):
1570+
ext = ".xyz"
1571+
temp_filexyz = tempfile.NamedTemporaryFile(dir=self.temp_dir,
1572+
prefix="Tmp2", suffix=ext)
1573+
os.chmod(temp_filexyz.name, stat.S_IXUSR)
1574+
self.addCleanup(temp_filexyz.close)
1575+
1576+
# strip path and extension
1577+
program = os.path.basename(temp_filexyz.name)
1578+
program = os.path.splitext(program)[0]
1579+
1580+
with support.EnvironmentVarGuard() as env:
1581+
env['PATHEXT'] = ext
1582+
rv = shutil.which(program, path=self.temp_dir)
1583+
self.assertEqual(rv, temp_filexyz.name)
1584+
15171585

15181586
class TestMove(unittest.TestCase):
15191587

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:func:`shutil.which` and :func:`distutils.spawn.find_executable` now use
2+
``os.confstr("CS_PATH")`` if available instead of :data:`os.defpath`, if the
3+
``PATH`` environment variable is not set. Moreover, don't use
4+
``os.confstr("CS_PATH")`` nor :data:`os.defpath` if the ``PATH`` environment
5+
variable is set to an empty string.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:func:`shutil.which` now uses ``os.confstr("CS_PATH")`` if available and if the
2+
:envvar:`PATH` environment variable is not set. Remove also the current
3+
directory from :data:`posixpath.defpath`. On Unix, :func:`shutil.which` and the
4+
:mod:`subprocess` module no longer search the executable in the current
5+
directory if the :envvar:`PATH` environment variable is not set.

0 commit comments

Comments
 (0)