Skip to content

Commit 96bcfe0

Browse files
committed
Added handling of nargs=argparse.REMAINDER in both AutoCompleter and ArgparseFunctor
Should correctly force all subsequent arguments to go to the REMAINDER argument once it is detected. Re-arranged the command generation in ArgparseFunctor to print flag arguments before positionals Also forces the remainder arguments to always be last.
1 parent 49cbec9 commit 96bcfe0

File tree

3 files changed

+118
-48
lines changed

3 files changed

+118
-48
lines changed

cmd2/argparse_completer.py

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
318318
flag_arg = AutoCompleter._ArgumentState()
319319
flag_action = None
320320

321+
remainder = {'arg': None, 'action': None}
322+
321323
matched_flags = []
322324
current_is_positional = False
323325
consumed_arg_values = {} # dict(arg_name -> [values, ...])
@@ -355,17 +357,76 @@ def consume_positional_argument() -> None:
355357
consumed_arg_values.setdefault(pos_action.dest, [])
356358
consumed_arg_values[pos_action.dest].append(token)
357359

360+
def process_action_nargs(action: argparse.Action, arg_state: AutoCompleter._ArgumentState) -> None:
361+
"""Process the current argparse Action and initialize the ArgumentState object used
362+
to track what arguments we have processed for this action"""
363+
if isinstance(action, _RangeAction):
364+
arg_state.min = action.nargs_min
365+
arg_state.max = action.nargs_max
366+
arg_state.variable = True
367+
if arg_state.min is None or arg_state.max is None:
368+
if action.nargs is None:
369+
arg_state.min = 1
370+
arg_state.max = 1
371+
elif action.nargs == '+':
372+
arg_state.min = 1
373+
arg_state.max = float('inf')
374+
arg_state.variable = True
375+
elif action.nargs == '*' or action.nargs == argparse.REMAINDER:
376+
arg_state.min = 0
377+
arg_state.max = float('inf')
378+
arg_state.variable = True
379+
if action.nargs == argparse.REMAINDER:
380+
print("Setting remainder")
381+
remainder['action'] = action
382+
remainder['arg'] = arg_state
383+
elif action.nargs == '?':
384+
arg_state.min = 0
385+
arg_state.max = 1
386+
arg_state.variable = True
387+
else:
388+
arg_state.min = action.nargs
389+
arg_state.max = action.nargs
390+
391+
392+
# This next block of processing tries to parse all parameters before the last parameter.
393+
# We're trying to determine what specific argument the current cursor positition should be
394+
# matched with. When we finish parsing all of the arguments, we can determine whether the
395+
# last token is a positional or flag argument and which specific argument it is.
396+
#
397+
# We're also trying to save every flag that has been used as well as every value that
398+
# has been used for a positional or flag parameter. By saving this information we can exclude
399+
# it from the completion results we generate for the last token. For example, single-use flag
400+
# arguments will be hidden from the list of available flags. Also, arguments with a
401+
# defined list of possible values will exclude values that have already been used.
402+
403+
# notes when the last token has been reached
358404
is_last_token = False
405+
359406
for idx, token in enumerate(tokens):
360407
is_last_token = idx >= len(tokens) - 1
361408
# Only start at the start token index
362409
if idx >= self._token_start_index:
410+
if remainder['arg'] is not None:
411+
print("In Remainder mode")
412+
if remainder['action'] == pos_action:
413+
consume_positional_argument()
414+
continue
415+
elif remainder['action'] == flag_action:
416+
consume_flag_argument()
417+
continue
418+
else:
419+
print("!!")
363420
current_is_positional = False
364421
# Are we consuming flag arguments?
365422
if not flag_arg.needed:
366-
# we're not consuming flag arguments, is the current argument a potential flag?
423+
# At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
424+
# If the argument is the start of a flag and this is the last token, we proceed forward to try
425+
# and match against our known flags.
426+
# If this argument is not the last token and the argument is exactly a flag prefix, then this
427+
# token should be consumed as an argument to a prior flag or positional argument.
367428
if len(token) > 0 and token[0] in self._parser.prefix_chars and\
368-
(is_last_token or (not is_last_token and token != '-')):
429+
(is_last_token or (not is_last_token and token not in self._parser.prefix_chars)):
369430
# reset some tracking values
370431
flag_arg.reset()
371432
# don't reset positional tracking because flags can be interspersed anywhere between positionals
@@ -381,7 +442,7 @@ def consume_positional_argument() -> None:
381442

382443
if flag_action is not None:
383444
# resolve argument counts
384-
self._process_action_nargs(flag_action, flag_arg)
445+
process_action_nargs(flag_action, flag_arg)
385446
if not is_last_token and not isinstance(flag_action, argparse._AppendAction):
386447
matched_flags.extend(flag_action.option_strings)
387448

@@ -418,7 +479,7 @@ def consume_positional_argument() -> None:
418479
return sub_completers[token].complete_command(tokens, text, line,
419480
begidx, endidx)
420481
pos_action = action
421-
self._process_action_nargs(pos_action, pos_arg)
482+
process_action_nargs(pos_action, pos_arg)
422483
consume_positional_argument()
423484

424485
elif not is_last_token and pos_arg.max is not None:
@@ -435,10 +496,12 @@ def consume_positional_argument() -> None:
435496
if not is_last_token and flag_arg.min is not None:
436497
flag_arg.needed = flag_arg.count < flag_arg.min
437498

499+
# Here we're done parsing all of the prior arguments. We know what the next argument is.
500+
438501
# if we don't have a flag to populate with arguments and the last token starts with
439502
# a flag prefix then we'll complete the list of flag options
440503
completion_results = []
441-
if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars:
504+
if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and remainder['arg'] is None:
442505
return AutoCompleter.basic_complete(text, line, begidx, endidx,
443506
[flag for flag in self._flags if flag not in matched_flags])
444507
# we're not at a positional argument, see if we're in a flag argument
@@ -522,32 +585,6 @@ def format_help(self, tokens: List[str]) -> str:
522585
return completers[token].format_help(tokens)
523586
return self._parser.format_help()
524587

525-
@staticmethod
526-
def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None:
527-
if isinstance(action, _RangeAction):
528-
arg_state.min = action.nargs_min
529-
arg_state.max = action.nargs_max
530-
arg_state.variable = True
531-
if arg_state.min is None or arg_state.max is None:
532-
if action.nargs is None:
533-
arg_state.min = 1
534-
arg_state.max = 1
535-
elif action.nargs == '+':
536-
arg_state.min = 1
537-
arg_state.max = float('inf')
538-
arg_state.variable = True
539-
elif action.nargs == '*':
540-
arg_state.min = 0
541-
arg_state.max = float('inf')
542-
arg_state.variable = True
543-
elif action.nargs == '?':
544-
arg_state.min = 0
545-
arg_state.max = 1
546-
arg_state.variable = True
547-
else:
548-
arg_state.min = action.nargs
549-
arg_state.max = action.nargs
550-
551588
def _complete_for_arg(self, action: argparse.Action,
552589
text: str,
553590
line: str,

cmd2/pyscript_bridge.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.Arg
7575

7676
# Dictionary mapping command argument name to value
7777
self._args = {}
78+
# tag the argument that's a remainder type
79+
self._remainder_arg = None
80+
# separately track flag arguments so they will be printed before positionals
81+
self._flag_args = []
7882
# argparse object for the current command layer
7983
self.__current_subcommand_parser = parser
8084

@@ -109,7 +113,6 @@ def __call__(self, *args, **kwargs):
109113
next_pos_index = 0
110114

111115
has_subcommand = False
112-
consumed_kw = []
113116

114117
# Iterate through the current sub-command's arguments in order
115118
for action in self.__current_subcommand_parser._actions:
@@ -118,7 +121,7 @@ def __call__(self, *args, **kwargs):
118121
# this is a flag argument, search for the argument by name in the parameters
119122
if action.dest in kwargs:
120123
self._args[action.dest] = kwargs[action.dest]
121-
consumed_kw.append(action.dest)
124+
self._flag_args.append(action.dest)
122125
else:
123126
# This is a positional argument, search the positional arguments passed in.
124127
if not isinstance(action, argparse._SubParsersAction):
@@ -157,6 +160,10 @@ def __call__(self, *args, **kwargs):
157160
elif action.nargs == '*':
158161
self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
159162
next_pos_index += pos_remain
163+
elif action.nargs == argparse.REMAINDER:
164+
self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
165+
next_pos_index += pos_remain
166+
self._remainder_arg = action.dest
160167
elif action.nargs == '?':
161168
self._args[action.dest] = args[next_pos_index]
162169
next_pos_index += 1
@@ -168,7 +175,7 @@ def __call__(self, *args, **kwargs):
168175

169176
# Check if there are any extra arguments we don't know how to handle
170177
for kw in kwargs:
171-
if kw not in self._args: # consumed_kw:
178+
if kw not in self._args:
172179
raise TypeError("{}() got an unexpected keyword argument '{}'".format(
173180
self.__current_subcommand_parser.prog, kw))
174181

@@ -214,27 +221,53 @@ def process_flag(action, value):
214221
if ' ' in item:
215222
item = '"{}"'.format(item)
216223
cmd_str[0] += '{} '.format(item)
224+
225+
# If this is a flag parameter that can accept a variable number of arguments and we have not
226+
# reached the max number, add a list completion suffix to tell argparse to move to the next
227+
# parameter
228+
if action.option_strings and isinstance(action, _RangeAction) \
229+
and action.nargs_max > len(value):
230+
cmd_str[0] += '{0}{0} '.format(self._parser.prefix_chars[0])
231+
217232
else:
218233
value = str(value).strip()
219234
if ' ' in value:
220235
value = '"{}"'.format(value)
221236
cmd_str[0] += '{} '.format(value)
222237

238+
# If this is a flag parameter that can accept a variable number of arguments and we have not
239+
# reached the max number, add a list completion suffix to tell argparse to move to the next
240+
# parameter
241+
if action.option_strings and isinstance(action, _RangeAction) \
242+
and action.nargs_max > 1:
243+
cmd_str[0] += '{0}{0} '.format(self._parser.prefix_chars[0])
244+
245+
def process_action(action):
246+
if isinstance(action, argparse._SubParsersAction):
247+
cmd_str[0] += '{} '.format(self._args[action.dest])
248+
traverse_parser(action.choices[self._args[action.dest]])
249+
elif isinstance(action, argparse._AppendAction):
250+
if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
251+
for values in self._args[action.dest]:
252+
process_flag(action, values)
253+
else:
254+
process_flag(action, self._args[action.dest])
255+
else:
256+
process_flag(action, self._args[action.dest])
257+
223258
def traverse_parser(parser):
259+
# first process optional flag arguments
224260
for action in parser._actions:
225-
# was something provided for the argument
226-
if action.dest in self._args:
227-
if isinstance(action, argparse._SubParsersAction):
228-
cmd_str[0] += '{} '.format(self._args[action.dest])
229-
traverse_parser(action.choices[self._args[action.dest]])
230-
elif isinstance(action, argparse._AppendAction):
231-
if isinstance(self._args[action.dest], list) or isinstance(self._args[action.dest], tuple):
232-
for values in self._args[action.dest]:
233-
process_flag(action, values)
234-
else:
235-
process_flag(action, self._args[action.dest])
236-
else:
237-
process_flag(action, self._args[action.dest])
261+
if action.dest in self._args and action.dest in self._flag_args and action.dest != self._remainder_arg:
262+
process_action(action)
263+
# next process positional arguments
264+
for action in parser._actions:
265+
if action.dest in self._args and action.dest not in self._flag_args and action.dest != self._remainder_arg:
266+
process_action(action)
267+
# Keep remainder argument last
268+
for action in parser._actions:
269+
if action.dest in self._args and action.dest == self._remainder_arg:
270+
process_action(action)
238271

239272
traverse_parser(self._parser)
240273

examples/tab_autocompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def _do_media_shows(self, args) -> None:
336336
movies_add_parser.add_argument('title', help='Movie Title')
337337
movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
338338
movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True)
339-
movies_add_parser.add_argument('actor', help='Actors', nargs='*')
339+
movies_add_parser.add_argument('actor', help='Actors', nargs=argparse.REMAINDER)
340340

341341
movies_delete_parser = movies_commands_subparsers.add_parser('delete')
342342
movies_delete_movie_id = movies_delete_parser.add_argument('movie_id', help='Movie ID')

0 commit comments

Comments
 (0)