From 690394fabc29908bbc4188ec29fd187fbb3ba13b Mon Sep 17 00:00:00 2001 From: Doug Jacobsen Date: Fri, 14 Apr 2023 11:13:17 -0600 Subject: [PATCH] Change environment modifications to escape with double quotes (#36789) This commit changes the environment modifications class to escape strings with double quotes instead of single quotes. Single quotes prevent the expansion of enviornment variables that are nested within environment variable definitions. --- lib/spack/spack/test/util/environment.py | 18 ++++++++++++++++ lib/spack/spack/util/environment.py | 27 +++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/spack/spack/test/util/environment.py b/lib/spack/spack/test/util/environment.py index 417dd2e337..eff07cbee9 100644 --- a/lib/spack/spack/test/util/environment.py +++ b/lib/spack/spack/test/util/environment.py @@ -151,3 +151,21 @@ def test_reverse_environment_modifications(working_env): start_env.pop("UNSET") assert os.environ == start_env + + +def test_escape_double_quotes_in_shell_modifications(): + to_validate = envutil.EnvironmentModifications() + + to_validate.set("VAR", "$PATH") + to_validate.append_path("VAR", "$ANOTHER_PATH") + + to_validate.set("QUOTED_VAR", '"MY_VAL"') + + cmds = to_validate.shell_modifications() + + if sys.platform != "win32": + assert 'export VAR="$PATH:$ANOTHER_PATH"' in cmds + assert r'export QUOTED_VAR="\"MY_VAL\""' in cmds + else: + assert "export VAR=$PATH;$ANOTHER_PATH" in cmds + assert r'export QUOTED_VAR="MY_VAL"' in cmds diff --git a/lib/spack/spack/util/environment.py b/lib/spack/spack/util/environment.py index 6be232bf42..64082ff313 100644 --- a/lib/spack/spack/util/environment.py +++ b/lib/spack/spack/util/environment.py @@ -12,7 +12,6 @@ import pickle import platform import re -import shlex import socket import sys from typing import Any, Callable, Dict, List, MutableMapping, Optional, Tuple, Union @@ -64,6 +63,26 @@ ModificationList = List[Union["NameModifier", "NameValueModifier"]] +_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.ASCII).search + + +def double_quote_escape(s): + """Return a shell-escaped version of the string *s*. + + This is similar to how shlex.quote works, but it escapes with double quotes + instead of single quotes, to allow environment variable expansion within + quoted strings. + """ + if not s: + return '""' + if _find_unsafe(s) is None: + return s + + # use double quotes, and escape double quotes in the string + # the string $"b is then quoted as "$\"b" + return '"' + s.replace('"', r"\"") + '"' + + def is_system_path(path: Path) -> bool: """Returns True if the argument is a system path, False otherwise.""" return bool(path) and (os.path.normpath(path) in SYSTEM_DIRS) @@ -135,7 +154,7 @@ def _env_var_to_source_line(var: str, val: str) -> str: fname=BASH_FUNCTION_FINDER.sub(r"\1", var), decl=val ) else: - source_line = f"{var}={shlex.quote(val)}; export {var}" + source_line = f"{var}={double_quote_escape(val)}; export {var}" return source_line @@ -649,7 +668,9 @@ def shell_modifications( cmds += _SHELL_UNSET_STRINGS[shell].format(name) else: if sys.platform != "win32": - cmd = _SHELL_SET_STRINGS[shell].format(name, shlex.quote(new_env[name])) + cmd = _SHELL_SET_STRINGS[shell].format( + name, double_quote_escape(new_env[name]) + ) else: cmd = _SHELL_SET_STRINGS[shell].format(name, new_env[name]) cmds += cmd