@@ -209,6 +209,34 @@ def register_custom_actions(parser: argparse.ArgumentParser) -> None:
209209 parser .register ('action' , 'append' , _AppendRangeAction )
210210
211211
212+ def token_resembles_flag (token : str , parser : argparse .ArgumentParser ) -> bool :
213+ """Determine if a token looks like a flag. Based on argparse._parse_optional()."""
214+ # if it's an empty string, it was meant to be a positional
215+ if not token :
216+ return False
217+
218+ # if it doesn't start with a prefix, it was meant to be positional
219+ if not token [0 ] in parser .prefix_chars :
220+ return False
221+
222+ # if it's just a single character, it was meant to be positional
223+ if len (token ) == 1 :
224+ return False
225+
226+ # if it looks like a negative number, it was meant to be positional
227+ # unless there are negative-number-like options
228+ if parser ._negative_number_matcher .match (token ):
229+ if not parser ._has_negative_number_optionals :
230+ return False
231+
232+ # if it contains a space, it was meant to be a positional
233+ if ' ' in token :
234+ return False
235+
236+ # Looks like a flag
237+ return True
238+
239+
212240class AutoCompleter (object ):
213241 """Automatically command line tab completion based on argparse parameters"""
214242
@@ -318,6 +346,9 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
318346 flag_arg = AutoCompleter ._ArgumentState ()
319347 flag_action = None
320348
349+ # dict is used because object wrapper is necessary to allow inner functions to modify outer variables
350+ remainder = {'arg' : None , 'action' : None }
351+
321352 matched_flags = []
322353 current_is_positional = False
323354 consumed_arg_values = {} # dict(arg_name -> [values, ...])
@@ -331,8 +362,8 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
331362 def consume_flag_argument () -> None :
332363 """Consuming token as a flag argument"""
333364 # we're consuming flag arguments
334- # if this is not empty and is not another potential flag, count towards flag arguments
335- if token and token [ 0 ] not in self ._parser . prefix_chars and flag_action is not None :
365+ # if the token does not look like a new flag, then count towards flag arguments
366+ if not token_resembles_flag ( token , self ._parser ) and flag_action is not None :
336367 flag_arg .count += 1
337368
338369 # does this complete a option item for the flag
@@ -355,17 +386,79 @@ def consume_positional_argument() -> None:
355386 consumed_arg_values .setdefault (pos_action .dest , [])
356387 consumed_arg_values [pos_action .dest ].append (token )
357388
389+ def process_action_nargs (action : argparse .Action , arg_state : AutoCompleter ._ArgumentState ) -> None :
390+ """Process the current argparse Action and initialize the ArgumentState object used
391+ to track what arguments we have processed for this action"""
392+ if isinstance (action , _RangeAction ):
393+ arg_state .min = action .nargs_min
394+ arg_state .max = action .nargs_max
395+ arg_state .variable = True
396+ if arg_state .min is None or arg_state .max is None :
397+ if action .nargs is None :
398+ arg_state .min = 1
399+ arg_state .max = 1
400+ elif action .nargs == '+' :
401+ arg_state .min = 1
402+ arg_state .max = float ('inf' )
403+ arg_state .variable = True
404+ elif action .nargs == '*' or action .nargs == argparse .REMAINDER :
405+ arg_state .min = 0
406+ arg_state .max = float ('inf' )
407+ arg_state .variable = True
408+ if action .nargs == argparse .REMAINDER :
409+ remainder ['action' ] = action
410+ remainder ['arg' ] = arg_state
411+ elif action .nargs == '?' :
412+ arg_state .min = 0
413+ arg_state .max = 1
414+ arg_state .variable = True
415+ else :
416+ arg_state .min = action .nargs
417+ arg_state .max = action .nargs
418+
419+ # This next block of processing tries to parse all parameters before the last parameter.
420+ # We're trying to determine what specific argument the current cursor positition should be
421+ # matched with. When we finish parsing all of the arguments, we can determine whether the
422+ # last token is a positional or flag argument and which specific argument it is.
423+ #
424+ # We're also trying to save every flag that has been used as well as every value that
425+ # has been used for a positional or flag parameter. By saving this information we can exclude
426+ # it from the completion results we generate for the last token. For example, single-use flag
427+ # arguments will be hidden from the list of available flags. Also, arguments with a
428+ # defined list of possible values will exclude values that have already been used.
429+
430+ # notes when the last token has been reached
358431 is_last_token = False
432+
359433 for idx , token in enumerate (tokens ):
360434 is_last_token = idx >= len (tokens ) - 1
361435 # Only start at the start token index
362436 if idx >= self ._token_start_index :
437+ # If a remainder action is found, force all future tokens to go to that
438+ if remainder ['arg' ] is not None :
439+ if remainder ['action' ] == pos_action :
440+ consume_positional_argument ()
441+ continue
442+ elif remainder ['action' ] == flag_action :
443+ consume_flag_argument ()
444+ continue
363445 current_is_positional = False
364446 # Are we consuming flag arguments?
365447 if not flag_arg .needed :
366- # we're not consuming flag arguments, is the current argument a potential flag?
367- if len (token ) > 0 and token [0 ] in self ._parser .prefix_chars and \
368- (is_last_token or (not is_last_token and token != '-' )):
448+ # Special case when each of the following is true:
449+ # - We're not in the middle of consuming flag arguments
450+ # - The current positional argument count has hit the max count
451+ # - The next positional argument is a REMAINDER argument
452+ # Argparse will now treat all future tokens as arguments to the positional including tokens that
453+ # look like flags so the completer should skip any flag related processing once this happens
454+ skip_flag = False
455+ if (pos_action is not None ) and pos_arg .count >= pos_arg .max and \
456+ next_pos_arg_index < len (self ._positional_actions ) and \
457+ self ._positional_actions [next_pos_arg_index ].nargs == argparse .REMAINDER :
458+ skip_flag = True
459+
460+ # At this point we're no longer consuming flag arguments. Is the current argument a potential flag?
461+ if token_resembles_flag (token , self ._parser ) and not skip_flag :
369462 # reset some tracking values
370463 flag_arg .reset ()
371464 # don't reset positional tracking because flags can be interspersed anywhere between positionals
@@ -381,7 +474,7 @@ def consume_positional_argument() -> None:
381474
382475 if flag_action is not None :
383476 # resolve argument counts
384- self . _process_action_nargs (flag_action , flag_arg )
477+ process_action_nargs (flag_action , flag_arg )
385478 if not is_last_token and not isinstance (flag_action , argparse ._AppendAction ):
386479 matched_flags .extend (flag_action .option_strings )
387480
@@ -418,7 +511,7 @@ def consume_positional_argument() -> None:
418511 return sub_completers [token ].complete_command (tokens , text , line ,
419512 begidx , endidx )
420513 pos_action = action
421- self . _process_action_nargs (pos_action , pos_arg )
514+ process_action_nargs (pos_action , pos_arg )
422515 consume_positional_argument ()
423516
424517 elif not is_last_token and pos_arg .max is not None :
@@ -435,10 +528,13 @@ def consume_positional_argument() -> None:
435528 if not is_last_token and flag_arg .min is not None :
436529 flag_arg .needed = flag_arg .count < flag_arg .min
437530
531+ # Here we're done parsing all of the prior arguments. We know what the next argument is.
532+
438533 # if we don't have a flag to populate with arguments and the last token starts with
439534 # a flag prefix then we'll complete the list of flag options
440535 completion_results = []
441- if not flag_arg .needed and len (tokens [- 1 ]) > 0 and tokens [- 1 ][0 ] in self ._parser .prefix_chars :
536+ if not flag_arg .needed and len (tokens [- 1 ]) > 0 and tokens [- 1 ][0 ] in self ._parser .prefix_chars and \
537+ remainder ['arg' ] is None :
442538 return AutoCompleter .basic_complete (text , line , begidx , endidx ,
443539 [flag for flag in self ._flags if flag not in matched_flags ])
444540 # we're not at a positional argument, see if we're in a flag argument
@@ -522,32 +618,6 @@ def format_help(self, tokens: List[str]) -> str:
522618 return completers [token ].format_help (tokens )
523619 return self ._parser .format_help ()
524620
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-
551621 def _complete_for_arg (self , action : argparse .Action ,
552622 text : str ,
553623 line : str ,
0 commit comments