summaryrefslogtreecommitdiff
path: root/src/tox/config
diff options
context:
space:
mode:
Diffstat (limited to 'src/tox/config')
-rw-r--r--src/tox/config/loader/ini/replace.py19
-rw-r--r--src/tox/config/loader/str_convert.py36
2 files changed, 47 insertions, 8 deletions
diff --git a/src/tox/config/loader/ini/replace.py b/src/tox/config/loader/ini/replace.py
index 40730ca5..41672780 100644
--- a/src/tox/config/loader/ini/replace.py
+++ b/src/tox/config/loader/ini/replace.py
@@ -30,7 +30,7 @@ LOGGER = logging.getLogger(__name__)
ARG_DELIMITER = ":"
REPLACE_START = "{"
REPLACE_END = "}"
-BACKSLASH_ESCAPE_CHARS = ["\\", ARG_DELIMITER, REPLACE_START, REPLACE_END, "[", "]"]
+BACKSLASH_ESCAPE_CHARS = [ARG_DELIMITER, REPLACE_START, REPLACE_END, "[", "]"]
MAX_REPLACE_DEPTH = 100
@@ -115,11 +115,18 @@ class MatchExpression:
pos = 0
while pos < len(value):
- if len(value) > pos + 1 and value[pos] == "\\" and value[pos + 1] in BACKSLASH_ESCAPE_CHARS:
- # backslash escapes the next character from a special set
- last_arg.append(value[pos + 1])
- pos += 2
- continue
+ if len(value) > pos + 1 and value[pos] == "\\":
+ if value[pos + 1] in BACKSLASH_ESCAPE_CHARS:
+ # backslash escapes the next character from a special set
+ last_arg.append(value[pos + 1])
+ pos += 2
+ continue
+ if value[pos + 1] == "\\":
+ # backlash doesn't escape a backslash, but does prevent it from affecting the next char
+ # a subsequent `shlex` pass will eat the double backslash during command splitting
+ last_arg.append(value[pos : pos + 2])
+ pos += 2
+ continue
fragment = value[pos:]
if terminator and fragment.startswith(terminator):
pos += len(terminator)
diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py
index f07545f5..ad7ffa5d 100644
--- a/src/tox/config/loader/str_convert.py
+++ b/src/tox/config/loader/str_convert.py
@@ -46,10 +46,42 @@ class StrConvert(Convert[str]):
raise TypeError(f"dictionary lines must be of form key=value, found {row!r}")
@staticmethod
+ def _win32_process_path_backslash(value: str, escape: str, special_chars: str) -> str:
+ """
+ Escape backslash in value that is not followed by a special character.
+
+ This allows windows paths to be written without double backslash, while
+ retaining the POSIX backslash escape semantics for quotes and escapes.
+ """
+ result = []
+ for ix, char in enumerate(value):
+ result.append(char)
+ if char == escape:
+ last_char = value[ix - 1 : ix]
+ if last_char == escape:
+ continue
+ next_char = value[ix + 1 : ix + 2]
+ if next_char not in (escape, *special_chars):
+ result.append(escape) # escape escapes that are not themselves escaping a special character
+ return "".join(result)
+
+ @staticmethod
def to_command(value: str) -> Command:
- is_win = sys.platform == "win32"
+ """
+ At this point, ``value`` has already been substituted out, and all punctuation / escapes are final.
+
+ Value will typically be stripped of whitespace when coming from an ini file.
+ """
value = value.replace(r"\#", "#")
- splitter = shlex.shlex(value, posix=not is_win)
+ is_win = sys.platform == "win32"
+ if is_win: # pragma: win32 cover
+ s = shlex.shlex(posix=True)
+ value = StrConvert._win32_process_path_backslash(
+ value,
+ escape=s.escape,
+ special_chars=s.quotes + s.whitespace,
+ )
+ splitter = shlex.shlex(value, posix=True)
splitter.whitespace_split = True
splitter.commenters = "" # comments handled earlier, and the shlex does not know escaped comment characters
args: list[str] = []