diff options
Diffstat (limited to 'src/tox/config')
-rw-r--r-- | src/tox/config/loader/ini/replace.py | 19 | ||||
-rw-r--r-- | src/tox/config/loader/str_convert.py | 36 |
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] = [] |