Skip to content

Commit ae961ae

Browse files
authored
[3.11] gh-124651: Quote template strings in venv activation scripts (GH-124712) (GH-126185) (#126269)
1 parent e84015f commit ae961ae

File tree

7 files changed

+135
-21
lines changed

7 files changed

+135
-21
lines changed

Lib/test/test_venv.py

+82-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import sys
1818
import sysconfig
1919
import tempfile
20-
from test.support import (captured_stdout, captured_stderr, requires_zlib,
20+
import shlex
21+
from test.support import (captured_stdout, captured_stderr,
2122
skip_if_broken_multiprocessing_synchronize, verbose,
2223
requires_subprocess, is_emscripten, is_wasi,
2324
requires_venv_with_pip, TEST_HOME_DIR,
@@ -96,6 +97,10 @@ def get_text_file_contents(self, *args, encoding='utf-8'):
9697
result = f.read()
9798
return result
9899

100+
def assertEndsWith(self, string, tail):
101+
if not string.endswith(tail):
102+
self.fail(f"String {string!r} does not end with {tail!r}")
103+
99104
class BasicTest(BaseTest):
100105
"""Test venv module functionality."""
101106

@@ -446,6 +451,82 @@ def test_executable_symlinks(self):
446451
'import sys; print(sys.executable)'])
447452
self.assertEqual(out.strip(), envpy.encode())
448453

454+
# gh-124651: test quoted strings
455+
@unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
456+
def test_special_chars_bash(self):
457+
"""
458+
Test that the template strings are quoted properly (bash)
459+
"""
460+
rmtree(self.env_dir)
461+
bash = shutil.which('bash')
462+
if bash is None:
463+
self.skipTest('bash required for this test')
464+
env_name = '"\';&&$e|\'"'
465+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
466+
builder = venv.EnvBuilder(clear=True)
467+
builder.create(env_dir)
468+
activate = os.path.join(env_dir, self.bindir, 'activate')
469+
test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
470+
with open(test_script, "w") as f:
471+
f.write(f'source {shlex.quote(activate)}\n'
472+
'python -c \'import sys; print(sys.executable)\'\n'
473+
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
474+
'deactivate\n')
475+
out, err = check_output([bash, test_script])
476+
lines = out.splitlines()
477+
self.assertTrue(env_name.encode() in lines[0])
478+
self.assertEndsWith(lines[1], env_name.encode())
479+
480+
# gh-124651: test quoted strings
481+
@unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
482+
def test_special_chars_csh(self):
483+
"""
484+
Test that the template strings are quoted properly (csh)
485+
"""
486+
rmtree(self.env_dir)
487+
csh = shutil.which('tcsh') or shutil.which('csh')
488+
if csh is None:
489+
self.skipTest('csh required for this test')
490+
env_name = '"\';&&$e|\'"'
491+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
492+
builder = venv.EnvBuilder(clear=True)
493+
builder.create(env_dir)
494+
activate = os.path.join(env_dir, self.bindir, 'activate.csh')
495+
test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
496+
with open(test_script, "w") as f:
497+
f.write(f'source {shlex.quote(activate)}\n'
498+
'python -c \'import sys; print(sys.executable)\'\n'
499+
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
500+
'deactivate\n')
501+
out, err = check_output([csh, test_script])
502+
lines = out.splitlines()
503+
self.assertTrue(env_name.encode() in lines[0])
504+
self.assertEndsWith(lines[1], env_name.encode())
505+
506+
# gh-124651: test quoted strings on Windows
507+
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
508+
def test_special_chars_windows(self):
509+
"""
510+
Test that the template strings are quoted properly on Windows
511+
"""
512+
rmtree(self.env_dir)
513+
env_name = "'&&^$e"
514+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
515+
builder = venv.EnvBuilder(clear=True)
516+
builder.create(env_dir)
517+
activate = os.path.join(env_dir, self.bindir, 'activate.bat')
518+
test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
519+
with open(test_batch, "w") as f:
520+
f.write('@echo off\n'
521+
f'"{activate}" & '
522+
f'{self.exe} -c "import sys; print(sys.executable)" & '
523+
f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
524+
'deactivate')
525+
out, err = check_output([test_batch])
526+
lines = out.splitlines()
527+
self.assertTrue(env_name.encode() in lines[0])
528+
self.assertEndsWith(lines[1], env_name.encode())
529+
449530
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
450531
def test_unicode_in_batch_file(self):
451532
"""

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

1617
CORE_VENV_DEPS = ('pip', 'setuptools')
@@ -394,11 +395,41 @@ def replace_variables(self, text, context):
394395
:param context: The information for the environment creation request
395396
being processed.
396397
"""
397-
text = text.replace('__VENV_DIR__', context.env_dir)
398-
text = text.replace('__VENV_NAME__', context.env_name)
399-
text = text.replace('__VENV_PROMPT__', context.prompt)
400-
text = text.replace('__VENV_BIN_NAME__', context.bin_name)
401-
text = text.replace('__VENV_PYTHON__', context.env_exe)
398+
replacements = {
399+
'__VENV_DIR__': context.env_dir,
400+
'__VENV_NAME__': context.env_name,
401+
'__VENV_PROMPT__': context.prompt,
402+
'__VENV_BIN_NAME__': context.bin_name,
403+
'__VENV_PYTHON__': context.env_exe,
404+
}
405+
406+
def quote_ps1(s):
407+
"""
408+
This should satisfy PowerShell quoting rules [1], unless the quoted
409+
string is passed directly to Windows native commands [2].
410+
[1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
411+
[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
412+
"""
413+
s = s.replace("'", "''")
414+
return f"'{s}'"
415+
416+
def quote_bat(s):
417+
return s
418+
419+
# gh-124651: need to quote the template strings properly
420+
quote = shlex.quote
421+
script_path = context.script_path
422+
if script_path.endswith('.ps1'):
423+
quote = quote_ps1
424+
elif script_path.endswith('.bat'):
425+
quote = quote_bat
426+
else:
427+
# fallbacks to POSIX shell compliant quote
428+
quote = shlex.quote
429+
430+
replacements = {key: quote(s) for key, s in replacements.items()}
431+
for key, quoted in replacements.items():
432+
text = text.replace(key, quoted)
402433
return text
403434

404435
def install_scripts(self, context, path):
@@ -438,6 +469,7 @@ def install_scripts(self, context, path):
438469
with open(srcfile, 'rb') as f:
439470
data = f.read()
440471
if not srcfile.endswith(('.exe', '.pdb')):
472+
context.script_path = srcfile
441473
try:
442474
data = data.decode('utf-8')
443475
data = self.replace_variables(data, context)

Lib/venv/scripts/common/activate

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ deactivate () {
3535
# unset irrelevant variables
3636
deactivate nondestructive
3737

38-
VIRTUAL_ENV="__VENV_DIR__"
38+
VIRTUAL_ENV=__VENV_DIR__
3939
export VIRTUAL_ENV
4040

4141
_OLD_VIRTUAL_PATH="$PATH"
42-
PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
42+
PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
4343
export PATH
4444

4545
# unset PYTHONHOME if set
@@ -52,9 +52,9 @@ fi
5252

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

Lib/venv/scripts/nt/activate.bat

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE (
88
"%SystemRoot%\System32\chcp.com" 65001 > nul
99
)
1010

11-
set VIRTUAL_ENV=__VENV_DIR__
11+
set "VIRTUAL_ENV=__VENV_DIR__"
1212

1313
if not defined PROMPT set PROMPT=$P$G
1414

@@ -24,8 +24,8 @@ set PYTHONHOME=
2424
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
2525
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
2626

27-
set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
28-
set VIRTUAL_ENV_PROMPT=__VENV_PROMPT__
27+
set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
28+
set "VIRTUAL_ENV_PROMPT=__VENV_PROMPT__"
2929

3030
:END
3131
if defined _OLD_CODEPAGE (

Lib/venv/scripts/posix/activate.csh

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ 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"
21-
setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
20+
set prompt = __VENV_PROMPT__"$prompt"
21+
setenv VIRTUAL_ENV_PROMPT __VENV_PROMPT__
2222
endif
2323

2424
alias pydoc python -m pydoc

Lib/venv/scripts/posix/activate.fish

+4-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ end
3333
# Unset irrelevant variables.
3434
deactivate nondestructive
3535

36-
set -gx VIRTUAL_ENV "__VENV_DIR__"
36+
set -gx VIRTUAL_ENV __VENV_DIR__
3737

3838
set -gx _OLD_VIRTUAL_PATH $PATH
39-
set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
39+
set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
4040

4141
# Unset PYTHONHOME if set.
4242
if set -q PYTHONHOME
@@ -56,7 +56,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
5656
set -l old_status $status
5757

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

6161
# Restore the return status of the previous command.
6262
echo "exit $old_status" | .
@@ -65,5 +65,5 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
6565
end
6666

6767
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
68-
set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
68+
set -gx VIRTUAL_ENV_PROMPT __VENV_PROMPT__
6969
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Properly quote template strings in :mod:`venv` activation scripts.

0 commit comments

Comments
 (0)