Skip to content

Commit 81979ee

Browse files
committed
Handle quotes around setting paths
1 parent 95b1f48 commit 81979ee

File tree

2 files changed

+83
-7
lines changed

2 files changed

+83
-7
lines changed

src/tox/config/__init__.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
_ENVSTR_SPLIT_PATTERN = re.compile(r"((?:\{[^}]+\})+)|,")
6363
_ENVSTR_EXPAND_PATTERN = re.compile(r"\{([^}]+)\}")
6464
_WHITESPACE_PATTERN = re.compile(r"\s+")
65+
_UNESCAPED_DOUBLEQUOTE = re.compile(r"((?<!\{1})'){2}")
6566

6667

6768
def get_plugin_manager(plugins=()):
@@ -1946,8 +1947,11 @@ def processcommand(cls, reader, command, replace=True):
19461947
continue
19471948

19481949
new_arg = ""
1950+
had_dual_quote = re.search(_UNESCAPED_DOUBLEQUOTE, word)
19491951
new_word = reader._replace(word, unquote_path=False)
19501952
new_word = reader._replace(new_word, unquote_path=False)
1953+
if not had_dual_quote:
1954+
new_word = re.sub(_UNESCAPED_DOUBLEQUOTE, "'", new_word)
19511955
new_word = new_word.replace("\\{", "{").replace("\\}", "}")
19521956
new_arg += new_word
19531957
newcommand += new_arg
@@ -1982,8 +1986,14 @@ def word_has_ended():
19821986
and ps.word
19831987
and ps.word[-1] not in string.whitespace
19841988
)
1985-
or (cur_char == "{" and ps.depth == 0 and not ps.word.endswith("\\"))
1986-
or (ps.depth == 0 and ps.word and ps.word[-1] == "}")
1989+
or (
1990+
cur_char == "{"
1991+
and ps.depth == 0
1992+
and not ps.word.endswith("\\")
1993+
and ps.word != "'"
1994+
)
1995+
or (ps.depth == 0 and ps.word and ps.word[-1] == "}" and peek() != "'")
1996+
or (ps.depth == 0 and ps.word and ps.word[-2:] == "}'")
19871997
or (cur_char not in string.whitespace and ps.word and ps.word.strip() == "")
19881998
)
19891999

@@ -1997,6 +2007,12 @@ def yield_if_word_ended():
19972007
if word_has_ended():
19982008
yield_this_word()
19992009

2010+
def peek():
2011+
try:
2012+
return self.command[_i + 1]
2013+
except IndexError:
2014+
return ""
2015+
20002016
def accumulate():
20012017
ps.word += cur_char
20022018

@@ -2006,7 +2022,7 @@ def push_substitution():
20062022
def pop_substitution():
20072023
ps.depth -= 1
20082024

2009-
for cur_char in self.command:
2025+
for _i, cur_char in enumerate(self.command):
20102026
if cur_char in string.whitespace:
20112027
if ps.depth == 0:
20122028
yield_if_word_ended()
@@ -2018,6 +2034,12 @@ def pop_substitution():
20182034
elif cur_char == "}":
20192035
accumulate()
20202036
pop_substitution()
2037+
elif cur_char == "'":
2038+
if ps.depth == 0 and ps.word[:2] == "'{" and ps.word[-1] == "}":
2039+
accumulate()
2040+
else:
2041+
yield_if_word_ended()
2042+
accumulate()
20212043
else:
20222044
yield_if_word_ended()
20232045
accumulate()

tests/unit/config/test_config.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ def test_command_substitution_pound(self, tmpdir, newconfig):
526526
toxworkdir = {toxinidir}/.tox#dir
527527
528528
[testenv:py27]
529-
commands = {envpython} {toxworkdir}
529+
commands = '{envpython}' '{toxworkdir}'{/}'foo#bar'
530530
""",
531531
)
532532

@@ -541,7 +541,7 @@ def test_command_substitution_pound(self, tmpdir, newconfig):
541541

542542
assert envconfig.commands[0] == [
543543
str(envconfig.envbindir.join("python")),
544-
str(config.toxworkdir.realpath()),
544+
str(config.toxworkdir.join("foo#bar")),
545545
]
546546

547547
def test_command_substitution_whitespace(self, tmpdir, newconfig):
@@ -552,7 +552,7 @@ def test_command_substitution_whitespace(self, tmpdir, newconfig):
552552
toxworkdir = {toxinidir}/.tox dir
553553
554554
[testenv:py27]
555-
commands = {envpython} {toxworkdir}
555+
commands = '{envpython}' '{toxworkdir}'{/}'foo bar'
556556
""",
557557
)
558558

@@ -567,7 +567,61 @@ def test_command_substitution_whitespace(self, tmpdir, newconfig):
567567

568568
assert envconfig.commands[0] == [
569569
str(envconfig.envbindir.join("python")),
570-
str(config.toxworkdir.realpath()),
570+
str(config.toxworkdir.join("foo bar").realpath()),
571+
]
572+
573+
def test_command_env_substitution_pound(self, tmpdir, newconfig):
574+
"""Ensure pound in path is kept in commands using setenv."""
575+
config = newconfig(
576+
"""
577+
[tox]
578+
toxworkdir = {toxinidir}/.tox#dir
579+
580+
[testenv:py27]
581+
setenv = VAR = '{toxworkdir}'{/}'foo#bar'
582+
commands = '{envpython}' '{env:VAR}'
583+
""",
584+
)
585+
586+
assert config.toxworkdir.realpath() == tmpdir.join(".tox#dir").realpath()
587+
588+
envconfig = config.envconfigs["py27"]
589+
590+
assert envconfig.envbindir.realpath() in [
591+
tmpdir.join(".tox#dir", "py27", "bin").realpath(),
592+
tmpdir.join(".tox#dir", "py27", "Scripts").realpath(),
593+
]
594+
595+
assert envconfig.commands[0] == [
596+
str(envconfig.envbindir.join("python")),
597+
str(config.toxworkdir.join("foo#bar").realpath()),
598+
]
599+
600+
def test_command_env_substitution_whitespace(self, tmpdir, newconfig):
601+
"""Ensure spaces in path is kept in commands using setenv."""
602+
config = newconfig(
603+
"""
604+
[tox]
605+
toxworkdir = {toxinidir}/.tox dir
606+
607+
[testenv:py27]
608+
setenv = VAR = '{toxworkdir}'{/}'foo bar'
609+
commands = '{envpython}' '{env:VAR}'
610+
""",
611+
)
612+
613+
assert config.toxworkdir.realpath() == tmpdir.join(".tox dir").realpath()
614+
615+
envconfig = config.envconfigs["py27"]
616+
617+
assert envconfig.envbindir.realpath() in [
618+
tmpdir.join(".tox dir", "py27", "bin").realpath(),
619+
tmpdir.join(".tox dir", "py27", "Scripts").realpath(),
620+
]
621+
622+
assert envconfig.commands[0] == [
623+
str(envconfig.envbindir.join("python")),
624+
str(config.toxworkdir.join("foo bar").realpath()),
571625
]
572626

573627

0 commit comments

Comments
 (0)