From 824f8d4fc0bd9845a247dc5c215d54fa17219a2d Mon Sep 17 00:00:00 2001 From: Anvesh <19anveshh@gmail.com> Date: Tue, 27 Oct 2026 01:10:33 +0530 Subject: [PATCH] gh-160732: protect venv prompts from expansion --- Lib/test/test_venv.py | 24 +++++++++++++++++++ Lib/venv/scripts/common/activate | 19 ++++++++++++++- ...7-00-00-00.gh-issue-160732.venv-prompt.rst | 2 ++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-27-00-00-00.gh-issue-160732.venv-prompt.rst diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 2f30d3108021dc6..5a0d3c793a3c908 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -770,6 +770,30 @@ def test_special_chars_bash(self): self.assertTrue(env_name.encode() in lines[0]) self.assertEndsWith(lines[1], env_name.encode()) + # gh-160732: prompt values must not be evaluated when bash displays PS1. + @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') + @unittest.skipIf(sys.platform == 'cygwin', 'fail to locate cygpython DLL') + def test_prompt_command_substitution_bash(self): + """Test that command substitutions in the prompt are not evaluated.""" + rmtree(self.env_dir) + bash = shutil.which('bash') + if bash is None: + self.skipTest('bash required for this test') + marker = os.path.join(self.env_dir, 'prompt-command-ran') + prompt = f'$(touch {shlex.quote(marker)})' + builder = venv.EnvBuilder(clear=True, prompt=prompt) + builder.create(self.env_dir) + activate = os.path.join(self.env_dir, self.bindir, 'activate') + result = subprocess.run( + [bash, '--noprofile', '--norc', '-i'], + input=f'source {shlex.quote(activate)}\nexit\n', + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual(result.returncode, 0) + self.assertFalse(os.path.exists(marker)) + # gh-124651: test quoted strings @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') @unittest.skipIf(sys.platform.startswith('netbsd'), diff --git a/Lib/venv/scripts/common/activate b/Lib/venv/scripts/common/activate index 657e4d481f6255a..8de23fa8ece2cbf 100644 --- a/Lib/venv/scripts/common/activate +++ b/Lib/venv/scripts/common/activate @@ -24,6 +24,11 @@ deactivate () { unset _OLD_VIRTUAL_PS1 fi + if [ -n "${_OLD_VIRTUAL_PSVAR+x}" ] ; then + psvar[9]="${_OLD_VIRTUAL_PSVAR}" + unset _OLD_VIRTUAL_PSVAR + fi + unset VIRTUAL_ENV unset VIRTUAL_ENV_PROMPT if [ ! "${1:-}" = "nondestructive" ] ; then @@ -66,7 +71,19 @@ fi if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then _OLD_VIRTUAL_PS1="${PS1:-}" - PS1="("__VENV_PROMPT__") ${PS1:-}" + if [ -n "${BASH_VERSION:-}" ] ; then + # Bash expands PS1 again when it displays the prompt. Keep the prompt + # in a variable so its contents are not interpreted as shell code. + PS1='(${VIRTUAL_ENV_PROMPT}) '"${PS1:-}" + elif [ -n "${ZSH_VERSION:-}" ] ; then + # zsh expands prompt substitutions in variable values. Use a psvar + # entry so the prompt is inserted as data instead. + _OLD_VIRTUAL_PSVAR="${psvar[9]-}" + psvar[9]="${VIRTUAL_ENV_PROMPT}" + PS1='(%9v) '"${PS1:-}" + else + PS1='(${VIRTUAL_ENV_PROMPT}) '"${PS1:-}" + fi fi # Call hash to forget past commands. Without forgetting diff --git a/Misc/NEWS.d/next/Library/2026-10-27-00-00-00.gh-issue-160732.venv-prompt.rst b/Misc/NEWS.d/next/Library/2026-10-27-00-00-00.gh-issue-160732.venv-prompt.rst new file mode 100644 index 000000000000000..846ebd3ecdd85dd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-27-00-00-00.gh-issue-160732.venv-prompt.rst @@ -0,0 +1,2 @@ +Fix command substitution in virtual environment prompts being evaluated by the +generated Bash activation script.