Skip to content

Commit 5b9bdc1

Browse files
vstinnermgorny
authored andcommitted
[3.9] pythongh-124651: Quote template strings in venv activation scripts (pythonGH-124712) (pythonGH-126185) (pythonGH-126269) (pythonGH-126301)
(cherry picked from commit ae961ae)
1 parent 7a7d93a commit 5b9bdc1

File tree

5 files changed

+127
-14
lines changed

5 files changed

+127
-14
lines changed

Lib/test/test_venv.py

+81
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import subprocess
1515
import sys
1616
import tempfile
17+
import shlex
1718
from test.support import (captured_stdout, captured_stderr, requires_zlib,
1819
can_symlink, EnvironmentVarGuard, rmtree,
1920
import_module,
@@ -85,6 +86,10 @@ def get_text_file_contents(self, *args):
8586
result = f.read()
8687
return result
8788

89+
def assertEndsWith(self, string, tail):
90+
if not string.endswith(tail):
91+
self.fail(f"String {string!r} does not end with {tail!r}")
92+
8893
class BasicTest(BaseTest):
8994
"""Test venv module functionality."""
9095

@@ -303,6 +308,82 @@ def test_executable_symlinks(self):
303308
'import sys; print(sys.executable)'])
304309
self.assertEqual(out.strip(), envpy.encode())
305310

311+
# gh-124651: test quoted strings
312+
@unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
313+
def test_special_chars_bash(self):
314+
"""
315+
Test that the template strings are quoted properly (bash)
316+
"""
317+
rmtree(self.env_dir)
318+
bash = shutil.which('bash')
319+
if bash is None:
320+
self.skipTest('bash required for this test')
321+
env_name = '"\';&&$e|\'"'
322+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
323+
builder = venv.EnvBuilder(clear=True)
324+
builder.create(env_dir)
325+
activate = os.path.join(env_dir, self.bindir, 'activate')
326+
test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
327+
with open(test_script, "w") as f:
328+
f.write(f'source {shlex.quote(activate)}\n'
329+
'python -c \'import sys; print(sys.executable)\'\n'
330+
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
331+
'deactivate\n')
332+
out, err = check_output([bash, test_script])
333+
lines = out.splitlines()
334+
self.assertTrue(env_name.encode() in lines[0])
335+
self.assertEndsWith(lines[1], env_name.encode())
336+
337+
# gh-124651: test quoted strings
338+
@unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
339+
def test_special_chars_csh(self):
340+
"""
341+
Test that the template strings are quoted properly (csh)
342+
"""
343+
rmtree(self.env_dir)
344+
csh = shutil.which('tcsh') or shutil.which('csh')
345+
if csh is None:
346+
self.skipTest('csh required for this test')
347+
env_name = '"\';&&$e|\'"'
348+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
349+
builder = venv.EnvBuilder(clear=True)
350+
builder.create(env_dir)
351+
activate = os.path.join(env_dir, self.bindir, 'activate.csh')
352+
test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
353+
with open(test_script, "w") as f:
354+
f.write(f'source {shlex.quote(activate)}\n'
355+
'python -c \'import sys; print(sys.executable)\'\n'
356+
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
357+
'deactivate\n')
358+
out, err = check_output([csh, test_script])
359+
lines = out.splitlines()
360+
self.assertTrue(env_name.encode() in lines[0])
361+
self.assertEndsWith(lines[1], env_name.encode())
362+
363+
# gh-124651: test quoted strings on Windows
364+
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
365+
def test_special_chars_windows(self):
366+
"""
367+
Test that the template strings are quoted properly on Windows
368+
"""
369+
rmtree(self.env_dir)
370+
env_name = "'&&^$e"
371+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
372+
builder = venv.EnvBuilder(clear=True)
373+
builder.create(env_dir)
374+
activate = os.path.join(env_dir, self.bindir, 'activate.bat')
375+
test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
376+
with open(test_batch, "w") as f:
377+
f.write('@echo off\n'
378+
f'"{activate}" & '
379+
f'{self.exe} -c "import sys; print(sys.executable)" & '
380+
f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
381+
'deactivate')
382+
out, err = check_output([test_batch])
383+
lines = out.splitlines()
384+
self.assertTrue(env_name.encode() in lines[0])
385+
self.assertEndsWith(lines[1], env_name.encode())
386+
306387
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
307388
def test_unicode_in_batch_file(self):
308389
"""

Lib/venv/__init__.py

+37-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sys
1212
import sysconfig
1313
import types
14+
import shlex
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -324,11 +325,41 @@ def replace_variables(self, text, context):
324325
:param context: The information for the environment creation request
325326
being processed.
326327
"""
327-
text = text.replace('__VENV_DIR__', context.env_dir)
328-
text = text.replace('__VENV_NAME__', context.env_name)
329-
text = text.replace('__VENV_PROMPT__', context.prompt)
330-
text = text.replace('__VENV_BIN_NAME__', context.bin_name)
331-
text = text.replace('__VENV_PYTHON__', context.env_exe)
328+
replacements = {
329+
'__VENV_DIR__': context.env_dir,
330+
'__VENV_NAME__': context.env_name,
331+
'__VENV_PROMPT__': context.prompt,
332+
'__VENV_BIN_NAME__': context.bin_name,
333+
'__VENV_PYTHON__': context.env_exe,
334+
}
335+
336+
def quote_ps1(s):
337+
"""
338+
This should satisfy PowerShell quoting rules [1], unless the quoted
339+
string is passed directly to Windows native commands [2].
340+
[1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
341+
[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
342+
"""
343+
s = s.replace("'", "''")
344+
return f"'{s}'"
345+
346+
def quote_bat(s):
347+
return s
348+
349+
# gh-124651: need to quote the template strings properly
350+
quote = shlex.quote
351+
script_path = context.script_path
352+
if script_path.endswith('.ps1'):
353+
quote = quote_ps1
354+
elif script_path.endswith('.bat'):
355+
quote = quote_bat
356+
else:
357+
# fallbacks to POSIX shell compliant quote
358+
quote = shlex.quote
359+
360+
replacements = {key: quote(s) for key, s in replacements.items()}
361+
for key, quoted in replacements.items():
362+
text = text.replace(key, quoted)
332363
return text
333364

334365
def install_scripts(self, context, path):
@@ -368,6 +399,7 @@ def install_scripts(self, context, path):
368399
with open(srcfile, 'rb') as f:
369400
data = f.read()
370401
if not srcfile.endswith(('.exe', '.pdb')):
402+
context.script_path = srcfile
371403
try:
372404
data = data.decode('utf-8')
373405
data = self.replace_variables(data, context)

Lib/venv/scripts/common/activate

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ deactivate () {
3737
# unset irrelevant variables
3838
deactivate nondestructive
3939

40-
VIRTUAL_ENV="__VENV_DIR__"
40+
VIRTUAL_ENV=__VENV_DIR__
4141
export VIRTUAL_ENV
4242

4343
_OLD_VIRTUAL_PATH="$PATH"
44-
PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
44+
PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
4545
export PATH
4646

4747
# unset PYTHONHOME if set
@@ -54,7 +54,7 @@ fi
5454

5555
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
5656
_OLD_VIRTUAL_PS1="${PS1:-}"
57-
PS1="__VENV_PROMPT__${PS1:-}"
57+
PS1=__VENV_PROMPT__"${PS1:-}"
5858
export PS1
5959
fi
6060

Lib/venv/scripts/posix/activate.csh

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
88
# Unset irrelevant variables.
99
deactivate nondestructive
1010

11-
setenv VIRTUAL_ENV "__VENV_DIR__"
11+
setenv VIRTUAL_ENV __VENV_DIR__
1212

1313
set _OLD_VIRTUAL_PATH="$PATH"
14-
setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
14+
setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
1515

1616

1717
set _OLD_VIRTUAL_PROMPT="$prompt"
1818

1919
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
20-
set prompt = "__VENV_PROMPT__$prompt"
20+
set prompt = __VENV_PROMPT__"$prompt"
2121
endif
2222

2323
alias pydoc python -m pydoc

Lib/venv/scripts/posix/activate.fish

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ end
2929
# Unset irrelevant variables.
3030
deactivate nondestructive
3131

32-
set -gx VIRTUAL_ENV "__VENV_DIR__"
32+
set -gx VIRTUAL_ENV __VENV_DIR__
3333

3434
set -gx _OLD_VIRTUAL_PATH $PATH
35-
set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
35+
set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
3636

3737
# Unset PYTHONHOME if set.
3838
if set -q PYTHONHOME
@@ -52,7 +52,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
5252
set -l old_status $status
5353

5454
# Output the venv prompt; color taken from the blue of the Python logo.
55-
printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal)
55+
printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
5656

5757
# Restore the return status of the previous command.
5858
echo "exit $old_status" | .

0 commit comments

Comments
 (0)