Skip to content

Commit 72b8e02

Browse files
author
Dillan Mills
committed
Fix issue with spaces causing option values to be duplicated as targets, SCons#3436, SCons#3798
1 parent f408511 commit 72b8e02

File tree

5 files changed

+281
-65
lines changed

5 files changed

+281
-65
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
4545

4646
From Dillan Mills:
4747
- Fix support for short options (`-x`).
48+
- Add support for long and short options with nargs != '?' to contain spaces:
49+
- `--extra A B C` and `-x A B C` will both work as expected
4850

4951

5052
RELEASE 4.0.1 - Mon, 16 Jul 2020 16:06:40 -0700

SCons/Script/SConsOptions.py

Lines changed: 24 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,14 @@ def _process_short_opts(self, rargs, values):
291291
raise
292292

293293
if option.takes_value():
294+
had_explicit_value = False
295+
294296
# Any characters left in arg? Pretend they're the
295297
# next arg, and stop consuming characters of arg.
296298
if i < len(arg):
297299
rargs.insert(0, arg[i:])
298300
stop = True
301+
had_explicit_value = True
299302

300303
nargs = option.nargs
301304
if len(rargs) < nargs:
@@ -306,9 +309,19 @@ def _process_short_opts(self, rargs, values):
306309
% (opt, nargs))
307310
elif nargs == 1:
308311
value = rargs.pop(0)
312+
if not had_explicit_value:
313+
SCons.Script._Remove_Target(value)
314+
if '=' in value:
315+
SCons.Script._Remove_Argument(value)
309316
else:
310317
value = tuple(rargs[0:nargs])
311318
del rargs[0:nargs]
319+
for i in range(len(value)):
320+
if not had_explicit_value or i > 0:
321+
SCons.Script._Remove_Target(value[i])
322+
if '=' in value[i]:
323+
SCons.Script._Remove_Argument(value[i])
324+
312325

313326
else: # option doesn't take a value
314327
value = None
@@ -379,9 +392,18 @@ def _process_long_opt(self, rargs, values):
379392
% (opt, nargs))
380393
elif nargs == 1:
381394
value = rargs.pop(0)
395+
if not had_explicit_value:
396+
SCons.Script._Remove_Target(value)
397+
if '=' in value:
398+
SCons.Script._Remove_Argument(value)
382399
else:
383400
value = tuple(rargs[0:nargs])
384401
del rargs[0:nargs]
402+
for i in range(len(value)):
403+
if not had_explicit_value or i > 0:
404+
SCons.Script._Remove_Target(value[i])
405+
if '=' in value[i]:
406+
SCons.Script._Remove_Argument(value[i])
385407

386408
elif had_explicit_value:
387409
self.error(_("%s option does not take a value") % opt)
@@ -391,69 +413,6 @@ def _process_long_opt(self, rargs, values):
391413

392414
option.process(opt, value, values, self)
393415

394-
def reparse_local_options(self):
395-
""" Re-parse the leftover command-line options.
396-
397-
Parse options stored in `self.largs`, so that any value
398-
overridden on the command line is immediately available
399-
if the user turns around and does a :func:`GetOption` right away.
400-
401-
We mimic the processing of the single args
402-
in the original OptionParser :func:`_process_args`, but here we
403-
allow exact matches for long-opts only (no partial argument names!).
404-
Otherwise there could be problems in :func:`add_local_option`
405-
below. When called from there, we try to reparse the
406-
command-line arguments that
407-
408-
1. haven't been processed so far (`self.largs`), but
409-
2. are possibly not added to the list of options yet.
410-
411-
So, when we only have a value for "--myargument" so far,
412-
a command-line argument of "--myarg=test" would set it,
413-
per the behaviour of :func:`_match_long_opt`,
414-
which allows for partial matches of the option name,
415-
as long as the common prefix appears to be unique.
416-
This would lead to further confusion, because we might want
417-
to add another option "--myarg" later on (see issue #2929).
418-
419-
"""
420-
rargs = []
421-
largs_restore = []
422-
# Loop over all remaining arguments
423-
skip = False
424-
for l in self.largs:
425-
if skip:
426-
# Accept all remaining arguments as they are
427-
largs_restore.append(l)
428-
else:
429-
if len(l) > 2 and l[0:2] == "--":
430-
# Check long option
431-
lopt = (l,)
432-
if "=" in l:
433-
# Split into option and value
434-
lopt = l.split("=", 1)
435-
436-
if lopt[0] in self._long_opt:
437-
# Argument is already known
438-
rargs.append('='.join(lopt))
439-
else:
440-
# Not known yet, so reject for now
441-
largs_restore.append('='.join(lopt))
442-
else:
443-
if l == "--" or l == "-":
444-
# Stop normal processing and don't
445-
# process the rest of the command-line opts
446-
largs_restore.append(l)
447-
skip = True
448-
else:
449-
rargs.append(l)
450-
451-
# Parse the filtered list
452-
self.parse_args(rargs, self.values)
453-
# Restore the list of remaining arguments for the
454-
# next call of AddOption/add_local_option...
455-
self.largs = self.largs + largs_restore
456-
457416
def add_local_option(self, *args, **kw):
458417
"""
459418
Adds a local option to the parser.
@@ -481,8 +440,8 @@ def add_local_option(self, *args, **kw):
481440
# available if the user turns around and does a GetOption()
482441
# right away.
483442
setattr(self.values.__defaults__, result.dest, result.default)
484-
self.reparse_local_options()
485-
443+
self.parse_args(self.largs, self.values)
444+
486445
return result
487446

488447
class SConsIndentedHelpFormatter(optparse.IndentedHelpFormatter):

SCons/Script/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ def _Add_Arguments(alist):
215215
ARGUMENTS[a] = b
216216
ARGLIST.append((a, b))
217217

218+
def _Remove_Argument(aarg):
219+
if aarg:
220+
a, b = aarg.split('=', 1)
221+
ARGUMENTS.pop(a, None)
222+
if (a, b) in ARGLIST:
223+
ARGLIST.remove((a, b))
224+
218225
def _Add_Targets(tlist):
219226
if tlist:
220227
COMMAND_LINE_TARGETS.extend(tlist)
@@ -225,6 +232,15 @@ def _Add_Targets(tlist):
225232
_build_plus_default._add_Default = _build_plus_default._do_nothing
226233
_build_plus_default._clear = _build_plus_default._do_nothing
227234

235+
def _Remove_Target(targ):
236+
if targ:
237+
if targ in COMMAND_LINE_TARGETS:
238+
COMMAND_LINE_TARGETS.remove(targ)
239+
if targ in BUILD_TARGETS:
240+
BUILD_TARGETS.remove(targ)
241+
if targ in _build_plus_default:
242+
_build_plus_default.remove(targ)
243+
228244
def _Set_Default_Targets_Has_Been_Called(d, fs):
229245
return DEFAULT_TARGETS
230246

test/AddOption/args-and-targets.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python
2+
#
3+
# __COPYRIGHT__
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included
14+
# in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17+
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
#
24+
25+
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
26+
27+
"""
28+
Verify that when an option is specified which takes args,
29+
those do not end up treated as targets.
30+
"""
31+
32+
import TestSCons
33+
34+
test = TestSCons.TestSCons()
35+
36+
test.write('SConstruct', """\
37+
env = Environment()
38+
AddOption('-x', '--extra',
39+
nargs=1,
40+
dest='extra',
41+
action='store',
42+
type='string',
43+
metavar='ARG1',
44+
default=(),
45+
help='An argument to the option')
46+
print(str(GetOption('extra')))
47+
print(COMMAND_LINE_TARGETS)
48+
""")
49+
50+
# arg using =
51+
test.run('-Q -q --extra=A TARG', status=1, stdout="A\n['TARG']\n")
52+
# arg not using =
53+
test.run('-Q -q --extra A TARG', status=1, stdout="A\n['TARG']\n")
54+
# short arg with space
55+
test.run('-Q -q -x A TARG', status=1, stdout="A\n['TARG']\n")
56+
# short arg with no space
57+
test.run('-Q -q -xA TARG', status=1, stdout="A\n['TARG']\n")
58+
59+
test.write('SConstruct', """\
60+
env = Environment()
61+
AddOption('-x', '--extra',
62+
nargs=2,
63+
dest='extra',
64+
action='append',
65+
type='string',
66+
metavar='ARG1',
67+
default=[],
68+
help='An argument to the option')
69+
print(str(GetOption('extra')))
70+
print(COMMAND_LINE_TARGETS)
71+
""")
72+
73+
# many args and opts
74+
test.run('-Q -q --extra=A B TARG1 -x C D TARG2 -xE F TARG3 --extra G H TARG4',
75+
status=1, stdout="[('A', 'B'), ('C', 'D'), ('E', 'F'), ('G', 'H')]\n['TARG1', 'TARG2', 'TARG3', 'TARG4']\n")
76+
77+
test.write('SConstruct', """\
78+
env = Environment()
79+
AddOption('-x', '--extra',
80+
nargs=1,
81+
dest='extra',
82+
action='store',
83+
type='string',
84+
metavar='ARG1',
85+
default=(),
86+
help='An argument to the option')
87+
print(str(GetOption('extra')))
88+
print(COMMAND_LINE_TARGETS)
89+
""")
90+
91+
# opt value and target are same name
92+
test.run('-Q -q --extra=TARG1 TARG1', status=1, stdout="TARG1\n['TARG1']\n")
93+
test.run('-Q -q --extra TARG1 TARG1', status=1, stdout="TARG1\n['TARG1']\n")
94+
test.run('-Q -q -xTARG1 TARG1', status=1, stdout="TARG1\n['TARG1']\n")
95+
test.run('-Q -q -x TARG1 TARG1', status=1, stdout="TARG1\n['TARG1']\n")
96+
97+
# equals in opt value
98+
test.run('-Q -q --extra=A=B TARG1', status=1, stdout="A=B\n['TARG1']\n")
99+
test.run('-Q -q --extra A=B TARG1', status=1, stdout="A=B\n['TARG1']\n")
100+
test.run('-Q -q -xA=B TARG1', status=1, stdout="A=B\n['TARG1']\n")
101+
test.run('-Q -q -x A=B TARG1', status=1, stdout="A=B\n['TARG1']\n")
102+
103+
104+
105+
test.pass_test()
106+
107+
# Local Variables:
108+
# tab-width:4
109+
# indent-tabs-mode:nil
110+
# End:
111+
# vim: set expandtab tabstop=4 shiftwidth=4:

0 commit comments

Comments
 (0)