@@ -209,6 +209,34 @@ def register_custom_actions(parser: argparse.ArgumentParser) -> None:
209
209
parser .register ('action' , 'append' , _AppendRangeAction )
210
210
211
211
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
+
212
240
class AutoCompleter (object ):
213
241
"""Automatically command line tab completion based on argparse parameters"""
214
242
@@ -318,6 +346,9 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
318
346
flag_arg = AutoCompleter ._ArgumentState ()
319
347
flag_action = None
320
348
349
+ # dict is used because object wrapper is necessary to allow inner functions to modify outer variables
350
+ remainder = {'arg' : None , 'action' : None }
351
+
321
352
matched_flags = []
322
353
current_is_positional = False
323
354
consumed_arg_values = {} # dict(arg_name -> [values, ...])
@@ -331,8 +362,8 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
331
362
def consume_flag_argument () -> None :
332
363
"""Consuming token as a flag argument"""
333
364
# 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 :
336
367
flag_arg .count += 1
337
368
338
369
# does this complete a option item for the flag
@@ -355,17 +386,79 @@ def consume_positional_argument() -> None:
355
386
consumed_arg_values .setdefault (pos_action .dest , [])
356
387
consumed_arg_values [pos_action .dest ].append (token )
357
388
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
358
431
is_last_token = False
432
+
359
433
for idx , token in enumerate (tokens ):
360
434
is_last_token = idx >= len (tokens ) - 1
361
435
# Only start at the start token index
362
436
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
363
445
current_is_positional = False
364
446
# Are we consuming flag arguments?
365
447
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 :
369
462
# reset some tracking values
370
463
flag_arg .reset ()
371
464
# don't reset positional tracking because flags can be interspersed anywhere between positionals
@@ -381,7 +474,7 @@ def consume_positional_argument() -> None:
381
474
382
475
if flag_action is not None :
383
476
# resolve argument counts
384
- self . _process_action_nargs (flag_action , flag_arg )
477
+ process_action_nargs (flag_action , flag_arg )
385
478
if not is_last_token and not isinstance (flag_action , argparse ._AppendAction ):
386
479
matched_flags .extend (flag_action .option_strings )
387
480
@@ -418,7 +511,7 @@ def consume_positional_argument() -> None:
418
511
return sub_completers [token ].complete_command (tokens , text , line ,
419
512
begidx , endidx )
420
513
pos_action = action
421
- self . _process_action_nargs (pos_action , pos_arg )
514
+ process_action_nargs (pos_action , pos_arg )
422
515
consume_positional_argument ()
423
516
424
517
elif not is_last_token and pos_arg .max is not None :
@@ -435,10 +528,13 @@ def consume_positional_argument() -> None:
435
528
if not is_last_token and flag_arg .min is not None :
436
529
flag_arg .needed = flag_arg .count < flag_arg .min
437
530
531
+ # Here we're done parsing all of the prior arguments. We know what the next argument is.
532
+
438
533
# if we don't have a flag to populate with arguments and the last token starts with
439
534
# a flag prefix then we'll complete the list of flag options
440
535
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 :
442
538
return AutoCompleter .basic_complete (text , line , begidx , endidx ,
443
539
[flag for flag in self ._flags if flag not in matched_flags ])
444
540
# 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:
522
618
return completers [token ].format_help (tokens )
523
619
return self ._parser .format_help ()
524
620
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
-
551
621
def _complete_for_arg (self , action : argparse .Action ,
552
622
text : str ,
553
623
line : str ,
0 commit comments