Skip to content

Commit 6cf21b8

Browse files
committed
Fix ListVariable with a space-containing value
Fix ListVariable handling of a quoted variable value containing spaces. As a side effect of splitting the former monolithic converter/validator for ListVariable into separate callbacks, it became possible for subst to be called twice. The ListVariable converter produces an instance of a _ListVariable container, and running subst on that result ends up losing information, so avoid doing so. While developing a test for this, it turned out the test framework also didn't handle a quoted argument containing a space, so that a test case passing arguments to scons via "run(arguments='...')" could end up with scons seeing a different (broken) command line than scons invoked with the same arguments typing to a shell prompt. A regex is now used to more accurately split the "arguments" parameter, and a unit test was added to the framework tests to validate. The framework fix had a side effect - it was possible that when run as part of the test suite, the Variables package could receive a value still wrapped in quotes, leading to string mismatches ('"with space"' is not equal to 'with space'), so ListVariable now strips wrapping quote marks. Also during testing it turned out that the earlier fix for SCons#4241, allowing a Variable to declare the value should not be subst'd, introduced problems for two types which assumed they would always be passed a string. With subst=False, they could be passed a default value that had been specified as a bool. Fixed to not fail on that. Fixes SCons#4585 Signed-off-by: Mats Wichmann <mats@linux.com>
1 parent 876bb06 commit 6cf21b8

File tree

13 files changed

+207
-81
lines changed

13 files changed

+207
-81
lines changed

CHANGES.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
2222
- env.Dump() now considers the "key" positional argument to be a varargs
2323
type (zero, one or many). However called, it returns a serialized
2424
result that looks like a dict. Previously, only one "key" was
25-
accepted. and unlike the zero-args case, it was be serialized
26-
to a string containing the value without the key. For example, if
25+
accepted, and unlike the zero-args case, it was be serialized
26+
to a string containing the value (without the key). For example, if
2727
"print(repr(env.Dump('CC'))" previously returned "'gcc'", it will now
2828
return "{'CC': 'gcc'}".
2929
- Add a timeout to test/ninja/default_targets.py - it's gotten stuck on
@@ -40,6 +40,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
4040
- Improve wording of manpage "Functions and Environment Methods" section.
4141
Make doc function signature style more consistent - tweaks to AddOption,
4242
DefaultEnvironment and Tool,.
43+
- Fix handling of ListVariable when supplying a quoted choice containing
44+
a space character (issue #4585).
4345

4446

4547
RELEASE 4.8.0 - Sun, 07 Jul 2024 17:22:20 -0700

RELEASE.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ FIXES
4646
to be avaiable. `BoolVariable`, `EnumVariable`, `ListVariable`,
4747
`PackageVariable` and `PathVariable` are added to `__all__`,
4848
so this form of import should now work again.
49+
- Fix handling of ListVariable when supplying a quoted choice containing
50+
a space character (issue #4585).
4951

5052
IMPROVEMENTS
5153
------------

SCons/Variables/BoolVariable.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
...
3333
"""
3434

35-
from typing import Tuple, Callable
35+
from typing import Callable, Tuple, Union
3636

3737
import SCons.Errors
3838

@@ -42,7 +42,7 @@
4242
FALSE_STRINGS = ('n', 'no', 'false', 'f', '0', 'off', 'none')
4343

4444

45-
def _text2bool(val: str) -> bool:
45+
def _text2bool(val: Union[str, bool]) -> bool:
4646
"""Convert boolean-like string to boolean.
4747
4848
If *val* looks like it expresses a bool-like value, based on
@@ -54,6 +54,9 @@ def _text2bool(val: str) -> bool:
5454
Raises:
5555
ValueError: if *val* cannot be converted to boolean.
5656
"""
57+
if isinstance(val, bool):
58+
# mainly for the subst=False case: default might be a bool
59+
return val
5760
lval = val.lower()
5861
if lval in TRUE_STRINGS:
5962
return True
@@ -63,7 +66,7 @@ def _text2bool(val: str) -> bool:
6366
raise ValueError(f"Invalid value for boolean variable: {val!r}")
6467

6568

66-
def _validator(key, val, env) -> None:
69+
def _validator(key: str, val, env) -> None:
6770
"""Validate that the value of *key* in *env* is a boolean.
6871
6972
Parameter *val* is not used in the check.

SCons/Variables/BoolVariableTests.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def test_converter(self) -> None:
4343
"""Test the BoolVariable converter"""
4444
opts = SCons.Variables.Variables()
4545
opts.Add(SCons.Variables.BoolVariable('test', 'test option help', False))
46-
4746
o = opts.options[0]
4847

4948
true_values = [
@@ -76,6 +75,17 @@ def test_converter(self) -> None:
7675
with self.assertRaises(ValueError):
7776
o.converter('x')
7877

78+
# Synthesize the case where the variable is created with subst=False:
79+
# Variables code won't subst before calling the converter,
80+
# and we might have pulled a bool from the option default.
81+
with self.subTest():
82+
x = o.converter(True)
83+
assert x, f"converter returned False for {t!r}"
84+
with self.subTest():
85+
x = o.converter(False)
86+
assert not x, f"converter returned False for {t!r}"
87+
88+
7989
def test_validator(self) -> None:
8090
"""Test the BoolVariable validator"""
8191
opts = SCons.Variables.Variables()

SCons/Variables/ListVariable.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def __init__(
8181
if allowedElems is None:
8282
allowedElems = []
8383
super().__init__([_f for _f in initlist if _f])
84+
# TODO: why sorted? don't we want to display in the order user gave?
8485
self.allowedElems = sorted(allowedElems)
8586

8687
def __cmp__(self, other):
@@ -118,7 +119,12 @@ def _converter(val, allowedElems, mapdict) -> _ListVariable:
118119
The arguments *allowedElems* and *mapdict* are non-standard
119120
for a :class:`Variables` converter: the lambda in the
120121
:func:`ListVariable` function arranges for us to be called correctly.
122+
123+
Incoming values ``all`` and ``none`` are recognized and converted
124+
into their expanded form.
121125
"""
126+
val = val.replace('"', '') # trim any wrapping quotes
127+
val = val.replace("'", '')
122128
if val == 'none':
123129
val = []
124130
elif val == 'all':
@@ -155,7 +161,7 @@ def _validator(key, val, env) -> None:
155161
allowedElems = env[key].allowedElems
156162
if isinstance(val, _ListVariable): # not substituted, use .data
157163
notAllowed = [v for v in val.data if v not in allowedElems]
158-
else: # val will be a string
164+
else: # presumably a string
159165
notAllowed = [v for v in val.split() if v not in allowedElems]
160166
if notAllowed:
161167
# Converter only synthesized 'all' and 'none', they are never

SCons/Variables/ListVariableTests.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2222
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2323

24+
"""Test List Variables elements."""
25+
2426
import copy
2527
import unittest
2628

@@ -50,18 +52,22 @@ def test_ListVariable(self) -> None:
5052
assert o.default == 'one,three'
5153

5254
def test_converter(self) -> None:
53-
"""Test the ListVariable converter"""
55+
"""Test the ListVariable converter.
56+
57+
There is now a separate validator (for a long time validation was
58+
in the converter), but it depends on the _ListVariable instance the
59+
converter creates, so it's easier to test them in the same function.
60+
"""
5461
opts = SCons.Variables.Variables()
5562
opts.Add(
5663
SCons.Variables.ListVariable(
5764
'test',
5865
'test option help',
59-
'all',
60-
['one', 'two', 'three'],
61-
{'ONE': 'one', 'TWO': 'two'},
66+
default='all',
67+
names=['one', 'two', 'three'],
68+
map={'ONE': 'one', 'TWO': 'two'},
6269
)
6370
)
64-
6571
o = opts.options[0]
6672

6773
x = o.converter('all')
@@ -110,10 +116,48 @@ def test_converter(self) -> None:
110116
# invalid value should convert (no change) without error
111117
x = o.converter('no_match')
112118
assert str(x) == 'no_match', x
113-
# ... and fail to validate
119+
120+
# validator checks
121+
122+
# first, the one we just set up
114123
with self.assertRaises(SCons.Errors.UserError):
115124
o.validator('test', 'no_match', {"test": x})
116125

126+
# now a new option, this time with a name w/ space in it (issue #4585)
127+
opts.Add(
128+
SCons.Variables.ListVariable(
129+
'test2',
130+
help='test2 option help',
131+
default='two',
132+
names=['one', 'two', 'three', 'four space'],
133+
)
134+
)
135+
o = opts.options[1]
136+
137+
def test_valid(opt, seq):
138+
"""Validate a ListVariable value.
139+
140+
Call the converter manually, since we need the _ListVariable
141+
object to pass to the validator - this would normally be done
142+
by the Variables.Update method.
143+
"""
144+
x = opt.converter(seq)
145+
self.assertIsNone(opt.validator(opt.key, x, {opt.key: x}))
146+
147+
with self.subTest():
148+
test_valid(o, 'one')
149+
with self.subTest():
150+
test_valid(o, 'one,two,three')
151+
with self.subTest():
152+
test_valid(o, 'all')
153+
with self.subTest():
154+
test_valid(o, 'none')
155+
with self.subTest():
156+
test_valid(o, 'four space')
157+
with self.subTest():
158+
test_valid(o, 'one,four space')
159+
160+
117161
def test_copy(self) -> None:
118162
"""Test copying a ListVariable like an Environment would"""
119163
opts = SCons.Variables.Variables()

SCons/Variables/PackageVariable.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"""
5252

5353
import os
54-
from typing import Callable, Optional, Tuple
54+
from typing import Callable, Optional, Tuple, Union
5555

5656
import SCons.Errors
5757

@@ -60,13 +60,16 @@
6060
ENABLE_STRINGS = ('1', 'yes', 'true', 'on', 'enable', 'search')
6161
DISABLE_STRINGS = ('0', 'no', 'false', 'off', 'disable')
6262

63-
def _converter(val):
63+
def _converter(val: Union[str, bool]) -> Union[str, bool]:
6464
"""Convert package variables.
6565
6666
Returns True or False if one of the recognized truthy or falsy
6767
values is seen, else return the value unchanged (expected to
6868
be a path string).
6969
"""
70+
if isinstance(val, bool):
71+
# mainly for the subst=False case: default might be a bool
72+
return val
7073
lval = val.lower()
7174
if lval in ENABLE_STRINGS:
7275
return True
@@ -75,7 +78,7 @@ def _converter(val):
7578
return val
7679

7780

78-
def _validator(key, val, env, searchfunc) -> None:
81+
def _validator(key: str, val, env, searchfunc) -> None:
7982
"""Validate package variable for valid path.
8083
8184
Checks that if a path is given as the value, that pathname actually exists.

SCons/Variables/PackageVariableTests.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ def test_converter(self) -> None:
8282
x = o.converter(str(False))
8383
assert not x, "converter returned a string when given str(False)"
8484

85+
# Synthesize the case where the variable is created with subst=False:
86+
# Variables code won't subst before calling the converter,
87+
# and we might have pulled a bool from the option default.
88+
with self.subTest():
89+
x = o.converter(True)
90+
assert x, f"converter returned False for {t!r}"
91+
with self.subTest():
92+
x = o.converter(False)
93+
assert not x, f"converter returned False for {t!r}"
94+
8595
def test_validator(self) -> None:
8696
"""Test the PackageVariable validator"""
8797
opts = SCons.Variables.Variables()

SCons/Variables/PathVariable.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,12 @@ class _PathVariableClass:
9393
"""
9494

9595
@staticmethod
96-
def PathAccept(key, val, env) -> None:
96+
def PathAccept(key: str, val, env) -> None:
9797
"""Validate path with no checking."""
9898
return
9999

100100
@staticmethod
101-
def PathIsDir(key, val, env) -> None:
101+
def PathIsDir(key: str, val, env) -> None:
102102
"""Validate path is a directory."""
103103
if os.path.isdir(val):
104104
return
@@ -109,7 +109,7 @@ def PathIsDir(key, val, env) -> None:
109109
raise SCons.Errors.UserError(msg)
110110

111111
@staticmethod
112-
def PathIsDirCreate(key, val, env) -> None:
112+
def PathIsDirCreate(key: str, val, env) -> None:
113113
"""Validate path is a directory, creating if needed."""
114114
if os.path.isdir(val):
115115
return
@@ -123,7 +123,7 @@ def PathIsDirCreate(key, val, env) -> None:
123123
raise SCons.Errors.UserError(msg) from exc
124124

125125
@staticmethod
126-
def PathIsFile(key, val, env) -> None:
126+
def PathIsFile(key: str, val, env) -> None:
127127
"""Validate path is a file."""
128128
if not os.path.isfile(val):
129129
if os.path.isdir(val):
@@ -133,7 +133,7 @@ def PathIsFile(key, val, env) -> None:
133133
raise SCons.Errors.UserError(msg)
134134

135135
@staticmethod
136-
def PathExists(key, val, env) -> None:
136+
def PathExists(key: str, val, env) -> None:
137137
"""Validate path exists."""
138138
if not os.path.exists(val):
139139
msg = f'Path for variable {key!r} does not exist: {val}'

SCons/Variables/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ def __lt__(self, other):
6161

6262
def __str__(self) -> str:
6363
"""Provide a way to "print" a Variable object."""
64-
return f"({self.key!r}, {self.aliases}, {self.help!r}, {self.default!r}, {self.validator}, {self.converter})"
64+
return (
65+
f"({self.key!r}, {self.aliases}, {self.help!r}, {self.default!r}, "
66+
f"validator={self.validator}, converter={self.converter})"
67+
)
6568

6669

6770
class Variables:
@@ -287,7 +290,13 @@ def Update(self, env, args: Optional[dict] = None) -> None:
287290
for option in self.options:
288291
if option.validator and option.key in values:
289292
if option.do_subst:
290-
value = env.subst('${%s}' % option.key)
293+
val = env[option.key]
294+
if not SCons.Util.is_String(val):
295+
# issue #4585: a _ListVariable should not be further
296+
# substituted, breaks on values with spaces.
297+
value = val
298+
else:
299+
value = env.subst('${%s}' % option.key)
291300
else:
292301
value = env[option.key]
293302
option.validator(option.key, value, env)

0 commit comments

Comments
 (0)