1010import inspect
1111import numbers
1212import shutil
13+ from collections import deque
1314from typing import Dict , List , Optional , Union
1415
1516from . import cmd2
1617from . import utils
17- from .ansi import ansi_safe_wcswidth , style_error
18+ from .ansi import ansi_aware_write , ansi_safe_wcswidth , style_error
1819from .argparse_custom import ATTR_CHOICES_CALLABLE , INFINITY , generate_range_error
1920from .argparse_custom import ATTR_SUPPRESS_TAB_HINT , ATTR_DESCRIPTIVE_COMPLETION_HEADER , ATTR_NARGS_RANGE
2021from .argparse_custom import ChoicesCallable , CompletionError , CompletionItem
@@ -116,13 +117,13 @@ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
116117 parent_tokens = dict ()
117118 self ._parent_tokens = parent_tokens
118119
119- self ._flags = [] # all flags in this command
120- self ._flag_to_action = {} # maps flags to the argparse action object
121- self ._positional_actions = [] # actions for positional arguments (by position index)
122- self ._subcommand_action = None # this will be set if self._parser has subcommands
120+ self ._flags = [] # all flags in this command
121+ self ._flag_to_action = {} # maps flags to the argparse action object
122+ self ._positional_actions = [] # actions for positional arguments (by position index)
123+ self ._subcommand_action = None # this will be set if self._parser has subcommands
123124
124125 # Start digging through the argparse structures.
125- # _actions is the top level container of parameter definitions
126+ # _actions is the top level container of parameter definitions
126127 for action in self ._parser ._actions :
127128 # if the parameter is flag based, it will have option_strings
128129 if action .option_strings :
@@ -143,9 +144,8 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
143144 if not tokens :
144145 return []
145146
146- # Count which positional argument index we're at now. Loop through all tokens on the command line so far
147- # Skip any flags or flag parameter tokens
148- next_pos_arg_index = 0
147+ # Positionals args that are left to parse
148+ remaining_positionals = deque (self ._positional_actions )
149149
150150 # This gets set to True when flags will no longer be processed as argparse flags
151151 # That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
@@ -163,12 +163,58 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
163163 # Keeps track of arguments we've seen and any tokens they consumed
164164 consumed_arg_values = dict () # dict(arg_name -> List[tokens])
165165
166+ # Completed mutually exclusive groups
167+ completed_mutex_groups = dict () # dict(argparse._MutuallyExclusiveGroup -> Action which completed group)
168+
166169 def consume_argument (arg_state : AutoCompleter ._ArgumentState ) -> None :
167170 """Consuming token as an argument"""
168171 arg_state .count += 1
169172 consumed_arg_values .setdefault (arg_state .action .dest , [])
170173 consumed_arg_values [arg_state .action .dest ].append (token )
171174
175+ def update_mutex_groups (arg_action : argparse .Action ) -> bool :
176+ """
177+ Check if an argument belongs to a mutually exclusive group and either mark that group
178+ as complete or print an error if the group has already been completed
179+ :param arg_action: the action of the argument
180+ :return: False if the group has already been completed and there is a conflict, otherwise True
181+ """
182+ # Check if this action is in a mutually exclusive group
183+ for group in self ._parser ._mutually_exclusive_groups :
184+ if arg_action in group ._group_actions :
185+
186+ # Check if the group this action belongs to has already been completed
187+ if group in completed_mutex_groups :
188+
189+ # If this is the action that completed the group, then there is no error
190+ # since it's allowed to appear on the command line more than once.
191+ completer_action = completed_mutex_groups [group ]
192+ if arg_action == completer_action :
193+ return True
194+
195+ error = style_error ("\n Error: argument {}: not allowed with argument {}\n " .
196+ format (argparse ._get_action_name (arg_action ),
197+ argparse ._get_action_name (completer_action )))
198+ self ._print_message (error )
199+ return False
200+
201+ # Mark that this action completed the group
202+ completed_mutex_groups [group ] = arg_action
203+
204+ # Don't tab complete any of the other args in the group
205+ for group_action in group ._group_actions :
206+ if group_action == arg_action :
207+ continue
208+ elif group_action in self ._flag_to_action .values ():
209+ matched_flags .extend (group_action .option_strings )
210+ elif group_action in remaining_positionals :
211+ remaining_positionals .remove (group_action )
212+
213+ # Arg can only be in one group, so we are done
214+ break
215+
216+ return True
217+
172218 #############################################################################################
173219 # Parse all but the last token
174220 #############################################################################################
@@ -222,14 +268,17 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
222268 action = self ._flag_to_action [candidates_flags [0 ]]
223269
224270 if action is not None :
271+ if not update_mutex_groups (action ):
272+ return []
273+
225274 if isinstance (action , (argparse ._AppendAction ,
226275 argparse ._AppendConstAction ,
227276 argparse ._CountAction )):
228277 # Flags with action set to append, append_const, and count can be reused
229278 # Therefore don't erase any tokens already consumed for this flag
230279 consumed_arg_values .setdefault (action .dest , [])
231280 else :
232- # This flag is not resusable , so mark that we've seen it
281+ # This flag is not reusable , so mark that we've seen it
233282 matched_flags .extend (action .option_strings )
234283
235284 # It's possible we already have consumed values for this flag if it was used
@@ -255,12 +304,9 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
255304 else :
256305 # If we aren't current tracking a positional, then get the next positional arg to handle this token
257306 if pos_arg_state is None :
258- pos_index = next_pos_arg_index
259- next_pos_arg_index += 1
260-
261- # Make sure we are still have positional arguments to fill
262- if pos_index < len (self ._positional_actions ):
263- action = self ._positional_actions [pos_index ]
307+ # Make sure we are still have positional arguments to parse
308+ if remaining_positionals :
309+ action = remaining_positionals .popleft ()
264310
265311 # Are we at a subcommand? If so, forward to the matching completer
266312 if action == self ._subcommand_action :
@@ -285,6 +331,10 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
285331
286332 # Check if we have a positional to consume this token
287333 if pos_arg_state is not None :
334+ # No need to check for an error since we remove a completed group's positional from
335+ # remaining_positionals which means this action can't belong to a completed mutex group
336+ update_mutex_groups (pos_arg_state .action )
337+
288338 consume_argument (pos_arg_state )
289339
290340 # No more flags are allowed if this is a REMAINDER argument
@@ -295,10 +345,9 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
295345 elif pos_arg_state .count >= pos_arg_state .max :
296346 pos_arg_state = None
297347
298- # Check if this a case in which we've finished all positionals before one that has nargs
299- # set to argparse.REMAINDER. At this point argparse allows no more flags to be processed.
300- if next_pos_arg_index < len (self ._positional_actions ) and \
301- self ._positional_actions [next_pos_arg_index ].nargs == argparse .REMAINDER :
348+ # Check if the next positional has nargs set to argparse.REMAINDER.
349+ # At this point argparse allows no more flags to be processed.
350+ if remaining_positionals and remaining_positionals [0 ].nargs == argparse .REMAINDER :
302351 skip_remaining_flags = True
303352
304353 #############################################################################################
@@ -338,12 +387,11 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
338387 return []
339388
340389 # Otherwise check if we have a positional to complete
341- elif pos_arg_state is not None or next_pos_arg_index < len ( self . _positional_actions ) :
390+ elif pos_arg_state is not None or remaining_positionals :
342391
343392 # If we aren't current tracking a positional, then get the next positional arg to handle this token
344393 if pos_arg_state is None :
345- pos_index = next_pos_arg_index
346- action = self ._positional_actions [pos_index ]
394+ action = remaining_positionals .popleft ()
347395 pos_arg_state = AutoCompleter ._ArgumentState (action )
348396
349397 try :
@@ -532,23 +580,11 @@ def _complete_for_arg(self, arg_action: argparse.Action,
532580
533581 return self ._format_completions (arg_action , results )
534582
535- @staticmethod
536- def _format_message_prefix (arg_action : argparse .Action ) -> str :
537- """Format the arg prefix text that appears before messages printed to the user"""
538- # Check if this is a flag
539- if arg_action .option_strings :
540- flags = ', ' .join (arg_action .option_strings )
541- param = ' ' + str (arg_action .dest ).upper ()
542- return '{}{}' .format (flags , param )
543-
544- # Otherwise this is a positional
545- else :
546- return '{}' .format (str (arg_action .dest ).upper ())
547-
548583 @staticmethod
549584 def _print_message (msg : str ) -> None :
550585 """Print a message instead of tab completions and redraw the prompt and input line"""
551- print (msg )
586+ import sys
587+ ansi_aware_write (sys .stdout , msg + '\n ' )
552588 rl_force_redisplay ()
553589
554590 def _print_arg_hint (self , arg_action : argparse .Action ) -> None :
@@ -558,47 +594,34 @@ def _print_arg_hint(self, arg_action: argparse.Action) -> None:
558594 """
559595 # Check if hinting is disabled
560596 suppress_hint = getattr (arg_action , ATTR_SUPPRESS_TAB_HINT , False )
561- if suppress_hint or arg_action .help == argparse .SUPPRESS or arg_action . dest == argparse . SUPPRESS :
597+ if suppress_hint or arg_action .help == argparse .SUPPRESS :
562598 return
563599
564- prefix = self ._format_message_prefix (arg_action )
565- prefix = ' {0: <{width}} ' .format (prefix , width = 20 )
566- pref_len = len (prefix )
567-
568- help_text = '' if arg_action .help is None else arg_action .help
569- help_lines = help_text .splitlines ()
570-
571- if len (help_lines ) == 1 :
572- self ._print_message ('\n Hint:\n {}{}\n ' .format (prefix , help_lines [0 ]))
573- else :
574- out_str = '\n {}' .format (prefix )
575- out_str += '\n {0: <{width}}' .format ('' , width = pref_len ).join (help_lines )
576- self ._print_message ('\n Hint:' + out_str + '\n ' )
600+ # Use the parser's help formatter to print just this action's help text
601+ formatter = self ._parser ._get_formatter ()
602+ formatter .start_section ("Hint" )
603+ formatter .add_argument (arg_action )
604+ formatter .end_section ()
605+ out_str = formatter .format_help ()
606+ self ._print_message ('\n ' + out_str )
577607
578608 def _print_unfinished_flag_error (self , flag_arg_state : _ArgumentState ) -> None :
579609 """
580610 Print an error during tab completion when the user has not finished the current flag
581611 :param flag_arg_state: information about the unfinished flag action
582612 """
583- prefix = self ._format_message_prefix (flag_arg_state .action )
584-
585- out_str = "\n Error:\n "
586- out_str += ' {0: <{width}} ' .format (prefix , width = 20 )
587- out_str += generate_range_error (flag_arg_state .min , flag_arg_state .max )
588-
589- out_str += ' ({} entered)' .format (flag_arg_state .count )
590- self ._print_message (style_error ('{}\n ' .format (out_str )))
613+ error = "\n Error: argument {}: {} ({} entered)\n " .\
614+ format (argparse ._get_action_name (flag_arg_state .action ),
615+ generate_range_error (flag_arg_state .min , flag_arg_state .max ),
616+ flag_arg_state .count )
617+ self ._print_message (style_error ('{}' .format (error )))
591618
592619 def _print_completion_error (self , arg_action : argparse .Action , completion_error : CompletionError ) -> None :
593620 """
594621 Print a CompletionError to the user
595622 :param arg_action: action being tab completed
596623 :param completion_error: error that occurred
597624 """
598- prefix = self ._format_message_prefix (arg_action )
599-
600- out_str = "\n Error:\n "
601- out_str += ' {0: <{width}} ' .format (prefix , width = 20 )
602- out_str += str (completion_error )
603-
604- self ._print_message (style_error ('{}\n ' .format (out_str )))
625+ error = ("\n Error tab completing {}:\n "
626+ " {}\n " .format (argparse ._get_action_name (arg_action ), str (completion_error )))
627+ self ._print_message (style_error ('{}' .format (error )))
0 commit comments