Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ae961ae

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

File tree

7 files changed

+135
-21
lines changed
Filter options

7 files changed

+135
-21
lines changed

‎Lib/test/test_venv.py

Copy file name to clipboardExpand all lines: Lib/test/test_venv.py
+82-1Lines changed: 82 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: Lib/venv/__init__.py
+37-5Lines changed: 37 additions & 5 deletions
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

Copy file name to clipboardExpand all lines: Lib/venv/scripts/common/activate
+4-4Lines changed: 4 additions & 4 deletions
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

Copy file name to clipboardExpand all lines: Lib/venv/scripts/nt/activate.bat
+3-3Lines changed: 3 additions & 3 deletions
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

Copy file name to clipboardExpand all lines: Lib/venv/scripts/posix/activate.csh
+4-4Lines changed: 4 additions & 4 deletions
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

Copy file name to clipboardExpand all lines: Lib/venv/scripts/posix/activate.fish
+4-4Lines changed: 4 additions & 4 deletions
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
+1Lines changed: 1 addition & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.