Skip to content

Commit e47ecbd

Browse files
gh-60346: Improve handling single-dash options in ArgumentParser.parse_known_args() (GH-114180)
1 parent 872cc99 commit e47ecbd

File tree

3 files changed

+57
-23
lines changed

3 files changed

+57
-23
lines changed

Lib/argparse.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,7 +2033,7 @@ def consume_optional(start_index):
20332033

20342034
# get the optional identified at this index
20352035
option_tuple = option_string_indices[start_index]
2036-
action, option_string, explicit_arg = option_tuple
2036+
action, option_string, sep, explicit_arg = option_tuple
20372037

20382038
# identify additional optionals in the same arg string
20392039
# (e.g. -xyz is the same as -x -y -z if no args are required)
@@ -2060,18 +2060,27 @@ def consume_optional(start_index):
20602060
and option_string[1] not in chars
20612061
and explicit_arg != ''
20622062
):
2063+
if sep or explicit_arg[0] in chars:
2064+
msg = _('ignored explicit argument %r')
2065+
raise ArgumentError(action, msg % explicit_arg)
20632066
action_tuples.append((action, [], option_string))
20642067
char = option_string[0]
20652068
option_string = char + explicit_arg[0]
2066-
new_explicit_arg = explicit_arg[1:] or None
20672069
optionals_map = self._option_string_actions
20682070
if option_string in optionals_map:
20692071
action = optionals_map[option_string]
2070-
explicit_arg = new_explicit_arg
2072+
explicit_arg = explicit_arg[1:]
2073+
if not explicit_arg:
2074+
sep = explicit_arg = None
2075+
elif explicit_arg[0] == '=':
2076+
sep = '='
2077+
explicit_arg = explicit_arg[1:]
2078+
else:
2079+
sep = ''
20712080
else:
2072-
msg = _('ignored explicit argument %r')
2073-
raise ArgumentError(action, msg % explicit_arg)
2074-
2081+
extras.append(char + explicit_arg)
2082+
stop = start_index + 1
2083+
break
20752084
# if the action expect exactly one argument, we've
20762085
# successfully matched the option; exit the loop
20772086
elif arg_count == 1:
@@ -2299,18 +2308,17 @@ def _parse_optional(self, arg_string):
22992308
# if the option string is present in the parser, return the action
23002309
if arg_string in self._option_string_actions:
23012310
action = self._option_string_actions[arg_string]
2302-
return action, arg_string, None
2311+
return action, arg_string, None, None
23032312

23042313
# if it's just a single character, it was meant to be positional
23052314
if len(arg_string) == 1:
23062315
return None
23072316

23082317
# if the option string before the "=" is present, return the action
2309-
if '=' in arg_string:
2310-
option_string, explicit_arg = arg_string.split('=', 1)
2311-
if option_string in self._option_string_actions:
2312-
action = self._option_string_actions[option_string]
2313-
return action, option_string, explicit_arg
2318+
option_string, sep, explicit_arg = arg_string.partition('=')
2319+
if sep and option_string in self._option_string_actions:
2320+
action = self._option_string_actions[option_string]
2321+
return action, option_string, sep, explicit_arg
23142322

23152323
# search through all possible prefixes of the option string
23162324
# and all actions in the parser for possible interpretations
@@ -2319,7 +2327,7 @@ def _parse_optional(self, arg_string):
23192327
# if multiple actions match, the option string was ambiguous
23202328
if len(option_tuples) > 1:
23212329
options = ', '.join([option_string
2322-
for action, option_string, explicit_arg in option_tuples])
2330+
for action, option_string, sep, explicit_arg in option_tuples])
23232331
args = {'option': arg_string, 'matches': options}
23242332
msg = _('ambiguous option: %(option)s could match %(matches)s')
23252333
self.error(msg % args)
@@ -2343,7 +2351,7 @@ def _parse_optional(self, arg_string):
23432351

23442352
# it was meant to be an optional but there is no such option
23452353
# in this parser (though it might be a valid option in a subparser)
2346-
return None, arg_string, None
2354+
return None, arg_string, None, None
23472355

23482356
def _get_option_tuples(self, option_string):
23492357
result = []
@@ -2353,34 +2361,31 @@ def _get_option_tuples(self, option_string):
23532361
chars = self.prefix_chars
23542362
if option_string[0] in chars and option_string[1] in chars:
23552363
if self.allow_abbrev:
2356-
if '=' in option_string:
2357-
option_prefix, explicit_arg = option_string.split('=', 1)
2358-
else:
2359-
option_prefix = option_string
2360-
explicit_arg = None
2364+
option_prefix, sep, explicit_arg = option_string.partition('=')
2365+
if not sep:
2366+
sep = explicit_arg = None
23612367
for option_string in self._option_string_actions:
23622368
if option_string.startswith(option_prefix):
23632369
action = self._option_string_actions[option_string]
2364-
tup = action, option_string, explicit_arg
2370+
tup = action, option_string, sep, explicit_arg
23652371
result.append(tup)
23662372

23672373
# single character options can be concatenated with their arguments
23682374
# but multiple character options always have to have their argument
23692375
# separate
23702376
elif option_string[0] in chars and option_string[1] not in chars:
23712377
option_prefix = option_string
2372-
explicit_arg = None
23732378
short_option_prefix = option_string[:2]
23742379
short_explicit_arg = option_string[2:]
23752380

23762381
for option_string in self._option_string_actions:
23772382
if option_string == short_option_prefix:
23782383
action = self._option_string_actions[option_string]
2379-
tup = action, option_string, short_explicit_arg
2384+
tup = action, option_string, '', short_explicit_arg
23802385
result.append(tup)
23812386
elif option_string.startswith(option_prefix):
23822387
action = self._option_string_actions[option_string]
2383-
tup = action, option_string, explicit_arg
2388+
tup = action, option_string, None, None
23842389
result.append(tup)
23852390

23862391
# shouldn't ever get here

Lib/test/test_argparse.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,6 +2274,34 @@ def test_parse_known_args(self):
22742274
(NS(foo=False, bar=0.5, w=7, x='b'), ['-W', '-X', 'Y', 'Z']),
22752275
)
22762276

2277+
def test_parse_known_args_with_single_dash_option(self):
2278+
parser = ErrorRaisingArgumentParser()
2279+
parser.add_argument('-k', '--known', action='count', default=0)
2280+
parser.add_argument('-n', '--new', action='count', default=0)
2281+
self.assertEqual(parser.parse_known_args(['-k', '-u']),
2282+
(NS(known=1, new=0), ['-u']))
2283+
self.assertEqual(parser.parse_known_args(['-u', '-k']),
2284+
(NS(known=1, new=0), ['-u']))
2285+
self.assertEqual(parser.parse_known_args(['-ku']),
2286+
(NS(known=1, new=0), ['-u']))
2287+
self.assertArgumentParserError(parser.parse_known_args, ['-k=u'])
2288+
self.assertEqual(parser.parse_known_args(['-uk']),
2289+
(NS(known=0, new=0), ['-uk']))
2290+
self.assertEqual(parser.parse_known_args(['-u=k']),
2291+
(NS(known=0, new=0), ['-u=k']))
2292+
self.assertEqual(parser.parse_known_args(['-kunknown']),
2293+
(NS(known=1, new=0), ['-unknown']))
2294+
self.assertArgumentParserError(parser.parse_known_args, ['-k=unknown'])
2295+
self.assertEqual(parser.parse_known_args(['-ku=nknown']),
2296+
(NS(known=1, new=0), ['-u=nknown']))
2297+
self.assertEqual(parser.parse_known_args(['-knew']),
2298+
(NS(known=1, new=1), ['-ew']))
2299+
self.assertArgumentParserError(parser.parse_known_args, ['-kn=ew'])
2300+
self.assertArgumentParserError(parser.parse_known_args, ['-k-new'])
2301+
self.assertArgumentParserError(parser.parse_known_args, ['-kn-ew'])
2302+
self.assertEqual(parser.parse_known_args(['-kne-w']),
2303+
(NS(known=1, new=1), ['-e-w']))
2304+
22772305
def test_dest(self):
22782306
parser = ErrorRaisingArgumentParser()
22792307
parser.add_argument('--foo', action='store_true')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix ArgumentParser inconsistent with parse_known_args.

0 commit comments

Comments
 (0)