1# -*- coding: utf-8 -*- # 2# Copyright 2013 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Calliope argparse intercepts and extensions. 17 18Calliope uses the argparse module for command line argument definition and 19parsing. It intercepts some argparse methods to provide enhanced runtime help 20document generation, command line usage help, error handling and argument group 21conflict analysis. 22 23The parser and intercepts are in these modules: 24 25 parser_extensions (this module) 26 27 Extends and intercepts argparse.ArgumentParser and the parser args 28 namespace to support Command.Run() method access to info added in the 29 Command.Args() method. 30 31 parser_arguments 32 33 Intercepts the basic argument objects and collects data for command flag 34 metrics reporting. 35 36 parser_errors 37 38 Error/exception classes for all Calliope arg parse errors. Errors derived 39 from ArgumentError have a payload used for metrics reporting. 40 41Intercepted argument definitions for a command and all its ancestor command 42groups are kept in a tree of ArgumentInterceptor nodes. Inner nodes have 43is_group==True and an arguments list of child nodes. Leaf nodes have 44is_group==False. ArgumentInterceptor keeps track of the arguments and flags 45specified on the command line in a set that is queried to verify the specified 46arguments against their definitions. For example, that a required argument has 47been specified, or that at most one flag in a mutually exclusive group has been 48specified. 49 50The collected info is also used to generate help markdown documents. The 51markdown is annotated with extra text that collates and describes argument 52attributes and groupings. For example, mutually exclusive, required, and nested 53groups. 54 55The intercepted args namespace object passed to the Command.Run() method adds 56methods to access/modify info collected during the parse. 57""" 58 59from __future__ import absolute_import 60from __future__ import division 61from __future__ import unicode_literals 62 63import abc 64import argparse 65import collections 66import io 67import itertools 68import os 69import re 70import sys 71 72from googlecloudsdk.calliope import arg_parsers 73from googlecloudsdk.calliope import base # pylint: disable=unused-import 74from googlecloudsdk.calliope import parser_arguments 75from googlecloudsdk.calliope import parser_errors 76from googlecloudsdk.calliope import suggest_commands 77from googlecloudsdk.calliope import usage_text 78from googlecloudsdk.core import argv_utils 79from googlecloudsdk.core import config 80from googlecloudsdk.core import log 81from googlecloudsdk.core import metrics 82from googlecloudsdk.core.console import console_attr 83from googlecloudsdk.core.console import console_io 84from googlecloudsdk.core.document_renderers import render_document 85from googlecloudsdk.core.updater import update_manager 86import six 87 88 89_HELP_SEARCH_HINT = """\ 90To search the help text of gcloud commands, run: 91 gcloud help -- SEARCH_TERMS""" 92 93 94class Namespace(argparse.Namespace): 95 """A custom subclass for parsed args. 96 97 Attributes: 98 _deepest_parser: ArgumentParser, The deepest parser for the last command 99 part. 100 _parsers: ArgumentParser, The list of all parsers for the command. 101 _specified_args: {dest: arg-name}, A map of dest names for known args 102 specified on the command line to arg names that have been scrubbed for 103 metrics. This dict accumulate across all subparsers. 104 """ 105 106 def __init__(self, **kwargs): 107 self._deepest_parser = None 108 self._parsers = [] 109 self._specified_args = {} 110 super(Namespace, self).__init__(**kwargs) 111 112 def _SetParser(self, parser): 113 """Sets the parser for the first part of the command.""" 114 self._deepest_parser = parser 115 116 def _GetParser(self): 117 """Returns the deepest parser for the command.""" 118 return self._deepest_parser 119 120 def _GetCommand(self): 121 """Returns the command for the deepest parser.""" 122 # pylint: disable=protected-access 123 return self._GetParser()._calliope_command 124 125 def _Execute(self, command, call_arg_complete=False): 126 """Executes command in the current CLI. 127 128 Args: 129 command: A list of command args to execute. 130 call_arg_complete: Enable arg completion if True. 131 132 Returns: 133 Returns the list of resources from the command. 134 """ 135 call_arg_complete = False 136 # pylint: disable=protected-access 137 return self._GetCommand()._cli_generator.Generate().Execute( 138 command, call_arg_complete=call_arg_complete) 139 140 def GetDisplayInfo(self): 141 """Returns the parser display_info.""" 142 # pylint: disable=protected-access 143 return self._GetCommand().ai.display_info 144 145 @property 146 def CONCEPTS(self): # pylint: disable=invalid-name 147 """The holder for concepts v1 arguments.""" 148 handler = self._GetCommand().ai.concept_handler 149 if handler is None: 150 return handler 151 handler.parsed_args = self 152 return handler 153 154 @property 155 def CONCEPT_ARGS(self): # pylint: disable=invalid-name 156 """The holder for concepts v2 arguments.""" 157 handler = self._GetCommand().ai.concepts 158 if handler is None: 159 return handler 160 handler.parsed_args = self 161 return handler 162 163 def GetSpecifiedArgNames(self): 164 """Returns the scrubbed names for args specified on the command line.""" 165 return sorted(self._specified_args.values()) 166 167 def GetSpecifiedArgs(self): 168 """Gets the argument names and values that were actually specified. 169 170 Returns: 171 {str: str}, A mapping of argument name to value. 172 """ 173 return { 174 name: getattr(self, dest, 'UNKNOWN') 175 for dest, name in six.iteritems(self._specified_args) 176 } 177 178 def IsSpecified(self, dest): 179 """Returns True if args.dest was specified on the command line. 180 181 Args: 182 dest: str, The dest name for the arg to check. 183 184 Raises: 185 UnknownDestinationException: If there is no registered arg for dest. 186 187 Returns: 188 True if args.dest was specified on the command line. 189 """ 190 if not hasattr(self, dest): 191 raise parser_errors.UnknownDestinationException( 192 'No registered arg for destination [{}].'.format(dest)) 193 return dest in self._specified_args 194 195 def IsKnownAndSpecified(self, dest): 196 """Returns True if dest is a known and args.dest was specified. 197 198 Args: 199 dest: str, The dest name for the arg to check. 200 201 Returns: 202 True if args.dest is a known argument was specified on the command line. 203 """ 204 return hasattr(self, dest) and (dest in self._specified_args) 205 206 def GetFlagArgument(self, name): 207 """Returns the flag argument object for name. 208 209 Args: 210 name: The flag name or Namespace destination. 211 212 Raises: 213 UnknownDestinationException: If there is no registered flag arg for name. 214 215 Returns: 216 The flag argument object for name. 217 """ 218 if name.startswith('--'): 219 dest = name[2:].replace('-', '_') 220 flag = name 221 else: 222 dest = name 223 flag = '--' + name.replace('_', '-') 224 ai = self._GetCommand().ai 225 for arg in ai.flag_args + ai.ancestor_flag_args: 226 if (dest == arg.dest or 227 arg.option_strings and flag == arg.option_strings[0]): 228 return arg 229 raise parser_errors.UnknownDestinationException( 230 'No registered flag arg for [{}].'.format(name)) 231 232 def GetPositionalArgument(self, name): 233 """Returns the positional argument object for name. 234 235 Args: 236 name: The Namespace metavar or destination. 237 238 Raises: 239 UnknownDestinationException: If there is no registered positional arg 240 for name. 241 242 Returns: 243 The positional argument object for name. 244 """ 245 dest = name.replace('-', '_').lower() 246 meta = name.replace('-', '_').upper() 247 for arg in self._GetCommand().ai.positional_args: 248 if isinstance(arg, type): 249 continue 250 if dest == arg.dest or meta == arg.metavar: 251 return arg 252 raise parser_errors.UnknownDestinationException( 253 'No registered positional arg for [{}].'.format(name)) 254 255 def GetFlag(self, dest): 256 """Returns the flag name registered to dest or None is dest is a positional. 257 258 Args: 259 dest: The dest of a registered argument. 260 261 Raises: 262 UnknownDestinationException: If no arg is registered for dest. 263 264 Returns: 265 The flag name registered to dest or None if dest is a positional. 266 """ 267 arg = self.GetFlagArgument(dest) 268 return arg.option_strings[0] if arg.option_strings else None 269 270 def GetValue(self, dest): 271 """Returns the value of the argument registered for dest. 272 273 Args: 274 dest: The dest of a registered argument. 275 276 Raises: 277 UnknownDestinationException: If no arg is registered for dest. 278 279 Returns: 280 The value of the argument registered for dest. 281 """ 282 try: 283 return getattr(self, dest) 284 except AttributeError: 285 raise parser_errors.UnknownDestinationException( 286 'No registered arg for destination [{}].'.format(dest)) 287 288 def MakeGetOrRaise(self, flag_name): 289 """Returns a function to get given flag value or raise if it is not set. 290 291 This is useful when given flag becomes required when another flag 292 is present. 293 294 Args: 295 flag_name: str, The flag_name name for the arg to check. 296 297 Raises: 298 parser_errors.RequiredError: if flag is not specified. 299 UnknownDestinationException: If there is no registered arg for flag_name. 300 301 Returns: 302 Function for accessing given flag value. 303 """ 304 def _Func(): 305 flag = flag_name[2:] if flag_name.startswith('--') else flag_name 306 flag_value = getattr(self, flag) 307 if flag_value is None and not self.IsSpecified(flag): 308 raise parser_errors.RequiredError(argument=flag_name) 309 return flag_value 310 311 return _Func 312 313 314class _ErrorContext(object): 315 """Context from the most recent ArgumentParser.error() call. 316 317 The context can be saved and used to reproduce the error() method call later 318 in the execution. Used to probe argparse errors for different argument 319 combinations. 320 321 Attributes: 322 message: The error message string. 323 parser: The parser where the error occurred. 324 error: The exception error value. 325 """ 326 327 def __init__(self, message, parser, error): 328 self.message = re.sub(r"\bu'", "'", message) 329 self.parser = parser 330 self.error = error 331 self.flags_locations = parser.flags_locations 332 333 def AddLocations(self, arg): 334 """Adds locaton info from context for arg if specified.""" 335 locations = self.flags_locations.get(arg) 336 if locations: 337 arg = '{} ({})'.format(arg, ','.join(sorted(locations))) 338 return arg 339 340 341class ArgumentParser(argparse.ArgumentParser): 342 """A custom subclass for arg parsing behavior. 343 344 This overrides the default argparse parser. 345 346 Attributes: 347 _args: Original argv passed to argparse. 348 _calliope_command: base._Command, The Calliope command or group for this 349 parser. 350 _error_context: The most recent self.error() method _ErrorContext. 351 _is_group: bool, True if _calliope_command is a group. 352 _probe_error: bool, True when parse_known_args() is probing argparse errors 353 captured in the self.error() method. 354 _remainder_action: action, The argument action for a -- ... remainder 355 argument, added by AddRemainderArgument. 356 _specified_args: {dest: arg-name}, A map of dest names for known args 357 specified on the command line to arg names that have been scrubbed for 358 metrics. This value is initialized and propagated to the deepest parser 359 namespace in parse_known_args() from specified args collected in 360 _get_values(). 361 """ 362 363 _args = None 364 365 def __init__(self, *args, **kwargs): 366 self._calliope_command = kwargs.pop('calliope_command') 367 # Would rather isinstance(self._calliope_command, CommandGroup) here but 368 # that would introduce a circular dependency on calliope.backend. 369 self._is_group = hasattr(self._calliope_command, 'commands') 370 self._remainder_action = None 371 self._specified_args = {} 372 self._error_context = None 373 self._probe_error = False 374 self.flags_locations = collections.defaultdict(set) 375 super(ArgumentParser, self).__init__(*args, **kwargs) 376 377 def _Error(self, error): 378 # self.error() wraps the standard argparse error() method. 379 self.error(context=_ErrorContext(console_attr.SafeText(error), self, error)) 380 381 def AddRemainderArgument(self, *args, **kwargs): 382 """Add an argument representing '--' followed by anything. 383 384 This argument is bound to the parser, so the parser can use its helper 385 methods to parse. 386 387 Args: 388 *args: The arguments for the action. 389 **kwargs: They keyword arguments for the action. 390 391 Raises: 392 ArgumentException: If there already is a Remainder Action bound to this 393 parser. 394 395 Returns: 396 The created action. 397 """ 398 if self._remainder_action: 399 self._Error(parser_errors.ArgumentException( 400 'There can only be one pass through argument.')) 401 kwargs['action'] = arg_parsers.RemainderAction 402 # pylint:disable=protected-access 403 self._remainder_action = self.add_argument(*args, **kwargs) 404 return self._remainder_action 405 406 def GetSpecifiedArgNames(self): 407 """Returns the scrubbed names for args specified on the command line.""" 408 return sorted(self._specified_args.values()) 409 410 def _AddLocations(self, arg, value=None): 411 """Adds file and line info from context for arg if specified.""" 412 if value and '=' not in arg: 413 argval = '{}={}'.format(arg, value) 414 else: 415 argval = arg 416 locations = self.flags_locations.get(argval) 417 if locations: 418 arg = '{} ({})'.format(argval, ','.join(sorted(locations))) 419 return arg 420 421 def _Suggest(self, unknown_args): 422 """Error out with a suggestion based on text distance for each unknown.""" 423 messages = [] 424 suggester = usage_text.TextChoiceSuggester() 425 # pylint:disable=protected-access, This is an instance of this class. 426 for flag in self._calliope_command.GetAllAvailableFlags(): 427 options = flag.option_strings 428 if options: 429 # This is a flag, add all its names as choices. 430 suggester.AddChoices(options) 431 # Add any aliases as choices as well, but suggest the primary name. 432 aliases = getattr(flag, 'suggestion_aliases', None) 433 if aliases: 434 suggester.AddAliases(aliases, options[0]) 435 436 suggestions = {} 437 for arg in unknown_args: 438 # Only do this for flag names. 439 if not isinstance(arg, six.string_types): 440 continue 441 # Strip the flag value if any from the suggestion. 442 flag = arg.split('=')[0] 443 if flag.startswith('--'): 444 suggestion = suggester.GetSuggestion(flag) 445 arg = self._AddLocations(arg) 446 else: 447 suggestion = None 448 if arg in messages: 449 continue 450 if self._ExistingFlagAlternativeReleaseTracks(flag): 451 existing_alternatives = self._ExistingFlagAlternativeReleaseTracks(flag) 452 messages.append('\n {} flag is available in one or more alternate ' 453 'release tracks. Try:\n'.format(flag)) 454 messages.append('\n '.join(existing_alternatives) +'\n') 455 if suggestion: 456 suggestions[arg] = suggestion 457 messages.append(arg + " (did you mean '{0}'?)".format(suggestion)) 458 else: 459 messages.append(arg) 460 461 # If there is a single arg, put it on the same line. If there are multiple 462 # add each on its own line for better clarity. 463 if len(messages) > 1: 464 separator, prefix = '\n ', '' 465 else: 466 separator, prefix = ' ', '\n\n' 467 # Always add a final message suggesting gcloud help. Set off with new line 468 # if this will be the only new line. 469 messages.append('{}{}'.format(prefix, _HELP_SEARCH_HINT)) 470 self._Error(parser_errors.UnrecognizedArgumentsError( 471 'unrecognized arguments:{0}{1}'.format( 472 separator, separator.join(messages)), 473 parser=self, 474 total_unrecognized=len(unknown_args), 475 total_suggestions=len(suggestions), 476 suggestions=suggestions)) 477 478 def _SetErrorContext(self, context): 479 """Sets the current error context to context -- called by self.error().""" 480 self._error_context = context 481 482 def _ParseKnownArgs(self, args, namespace, wrapper=True): 483 """Calls parse_known_args() and adds error_context to the return. 484 485 Args: 486 args: The list of command line args. 487 namespace: The parsed args namespace. 488 wrapper: Calls the parse_known_args() wrapper if True, otherwise the 489 wrapped argparse parse_known_args(). 490 491 Returns: 492 namespace: The parsed arg namespace. 493 unknown_args: The list of unknown args. 494 error_context: The _ErrorContext if there was an error, None otherwise. 495 """ 496 self._error_context = None 497 parser = self if wrapper else super(ArgumentParser, self) 498 namespace, unknown_args = ( 499 parser.parse_known_args(args, namespace) or (namespace, [])) 500 error_context = self._error_context 501 self._error_context = None 502 if not unknown_args and hasattr(parser, 'flags_locations'): 503 parser.flags_locations = collections.defaultdict(set) 504 return namespace, unknown_args, error_context 505 506 def _DeduceBetterError(self, context, args, namespace): 507 """There is an argparse error in context, see if we can do better. 508 509 We are committed to an argparse error. See if we can do better than the 510 observed error in context by isolating each flag arg to determine if the 511 argparse error complained about a flag arg value instead of a positional. 512 Accumulate required flag args to ensure that all valid flag args are 513 checked. 514 515 Args: 516 context: The _ErrorContext containing the error to improve. 517 args: The subset of the command lines args that triggered the argparse 518 error in context. 519 namespace: The namespace for the current parser. 520 """ 521 self._probe_error = True 522 required = [] 523 skip = False 524 for arg in args: 525 if skip: 526 skip = False 527 required.append(arg) 528 continue 529 try: 530 if not arg.startswith('-'): 531 break 532 except AttributeError: 533 break 534 _, _, error_context = self._ParseKnownArgs(required + [arg], namespace) 535 if not error_context: 536 continue 537 if 'is required' in error_context.message: 538 required.append(arg) 539 if '=' in arg: 540 skip = True 541 elif 'too few arguments' not in error_context.message: 542 context = error_context 543 break 544 self._probe_error = False 545 context.error.argument = context.AddLocations(context.error.argument) 546 context.parser.error(context=context, reproduce=True) 547 548 @staticmethod 549 def GetDestinations(args): 550 """Returns the set of 'dest' attributes (or the arg if no dest).""" 551 return set([getattr(a, 'dest', a) for a in args]) 552 553 # pylint: disable=invalid-name, argparse style 554 def validate_specified_args(self, ai, specified_args, namespace, 555 is_required=True, top=True): 556 """Validate specified args against the arg group constraints. 557 558 Each group may be mutually exclusive and/or required. Each argument may be 559 required. 560 561 Args: 562 ai: ArgumentInterceptor, The argument interceptor containing the 563 ai.arguments argument group. 564 specified_args: set, The dests of the specified args. 565 namespace: object, The parsed args namespace. 566 is_required: bool, True if all containing groups are required. 567 top: bool, True if ai.arguments is the top level group. 568 569 Raises: 570 ModalGroupError: If modal arg not specified. 571 OptionalMutexError: On optional mutex group conflict. 572 RequiredError: If required arg not specified. 573 RequiredMutexError: On required mutex group conflict. 574 575 Returns: 576 True if the subgroup was specified. 577 """ 578 # TODO(b/120132521) Replace and eliminate argparse extensions 579 also_optional = [] # The optional args in group that were not specified. 580 have_optional = [] # The specified optional (not required) args. 581 have_required = [] # The specified required args. 582 need_required = [] # The required args in group that must be specified. 583 for arg in sorted(ai.arguments, key=usage_text.GetArgSortKey): 584 if arg.is_group: 585 arg_was_specified = self.validate_specified_args( 586 arg, 587 specified_args, 588 namespace, 589 is_required=is_required and arg.is_required, 590 top=False) 591 else: 592 arg_was_specified = arg.dest in specified_args 593 if arg_was_specified: 594 if arg.is_required: 595 have_required.append(arg) 596 else: 597 have_optional.append(arg) 598 elif arg.is_required: 599 if not isinstance(arg, DynamicPositionalAction): 600 need_required.append(arg) 601 else: 602 also_optional.append(arg) 603 604 if need_required: 605 if top or have_required and not (have_optional or also_optional): 606 ai = parser_arguments.ArgumentInterceptor(self, arguments=need_required) 607 self._Error(parser_errors.RequiredError( 608 parser=self, 609 argument=usage_text.GetArgUsage( 610 ai, value=False, hidden=True, top=top))) 611 if have_optional or have_required: 612 have_ai = parser_arguments.ArgumentInterceptor( 613 self, arguments=have_optional + have_required) 614 need_ai = parser_arguments.ArgumentInterceptor( 615 self, arguments=need_required) 616 self._Error(parser_errors.ModalGroupError( 617 parser=self, 618 argument=usage_text.GetArgUsage( 619 have_ai, value=False, hidden=True, top=top), 620 conflict=usage_text.GetArgUsage( 621 need_ai, value=False, hidden=True, top=top))) 622 623 # Multiple args with the same dest are counted as 1 arg. 624 count = (len(self.GetDestinations(have_required)) + 625 len(self.GetDestinations(have_optional))) 626 627 if ai.is_mutex: 628 conflict = usage_text.GetArgUsage(ai, value=False, hidden=True, top=top) 629 if is_required and ai.is_required: 630 if count != 1: 631 if count: 632 argument = usage_text.GetArgUsage( 633 sorted(have_required + have_optional, 634 key=usage_text.GetArgSortKey)[0], 635 value=False, hidden=True, top=top) 636 try: 637 flag = namespace.GetFlagArgument(argument) 638 except parser_errors.UnknownDestinationException: 639 flag = None 640 if flag: 641 value = namespace.GetValue(flag.dest) 642 if not isinstance(value, (bool, dict, list)): 643 argument = self._AddLocations(argument, value) 644 else: 645 argument = None 646 self._Error(parser_errors.RequiredMutexError( 647 parser=self, argument=argument, conflict=conflict)) 648 elif count > 1: 649 argument = usage_text.GetArgUsage( 650 sorted(have_required + have_optional, 651 key=usage_text.GetArgSortKey)[0], 652 value=False, hidden=True, top=top) 653 self._Error(parser_errors.OptionalMutexError( 654 parser=self, argument=argument, conflict=conflict)) 655 656 return bool(count) 657 658 def parse_known_args(self, args=None, namespace=None): 659 """Overrides argparse.ArgumentParser's .parse_known_args method.""" 660 if args is None: 661 args = argv_utils.GetDecodedArgv()[1:] 662 if namespace is None: 663 namespace = Namespace() 664 namespace._SetParser(self) # pylint: disable=protected-access 665 try: 666 if self._remainder_action: 667 # Remove remainder_action if still there so it is not parsed regularly. 668 try: 669 self._actions.remove(self._remainder_action) 670 except ValueError: 671 pass 672 # Split on first -- if it exists 673 namespace, args = self._remainder_action.ParseKnownArgs(args, namespace) 674 # _get_values() updates self._specified_args. 675 self._specified_args = namespace._specified_args # pylint: disable=protected-access 676 namespace, unknown_args, error_context = self._ParseKnownArgs( 677 args, namespace, wrapper=False) 678 # Propagate _specified_args. 679 namespace._specified_args.update(self._specified_args) # pylint: disable=protected-access 680 if unknown_args: 681 self._Suggest(unknown_args) 682 elif error_context: 683 if self._probe_error: 684 return 685 error_context.parser._DeduceBetterError( # pylint: disable=protected-access 686 error_context, args, namespace) 687 namespace._parsers.append(self) # pylint: disable=protected-access 688 finally: 689 # Replace action for help message and ArgumentErrors. 690 if self._remainder_action: 691 self._actions.append(self._remainder_action) 692 return (namespace, unknown_args) 693 694 @classmethod 695 def _SaveOriginalArgs(cls, original_args): 696 if original_args: 697 cls._args = original_args[:] 698 else: 699 cls._args = None 700 701 @classmethod 702 def _ClearOriginalArgs(cls): 703 cls._args = None 704 705 @classmethod 706 def _GetOriginalArgs(cls): 707 return cls._args 708 709 def parse_args(self, args=None, namespace=None): 710 """Overrides argparse.ArgumentParser's .parse_args method.""" 711 self._SaveOriginalArgs(args) 712 namespace, unknown_args, _ = self._ParseKnownArgs(args, namespace) 713 714 # pylint:disable=protected-access 715 deepest_parser = namespace._GetParser() 716 deepest_parser._specified_args = namespace._specified_args 717 718 if not unknown_args: 719 # All of the specified args from all of the subparsers are now known. 720 # Check for argument/group conflicts and error out from the deepest 721 # parser so the resulting error message has the correct command context. 722 for parser in namespace._parsers: 723 try: 724 # pylint: disable=protected-access 725 parser.validate_specified_args( 726 parser.ai, namespace._specified_args, namespace) 727 except argparse.ArgumentError as e: 728 deepest_parser._Error(e) 729 if namespace._GetCommand().is_group: 730 deepest_parser.error('Command name argument expected.') 731 732 # No argument/group conflicts. 733 return namespace 734 735 if deepest_parser._remainder_action: 736 # Assume the user wanted to pass all arguments after last recognized 737 # arguments into _remainder_action. Either do this with a warning or 738 # fail depending on strictness. 739 # pylint:disable=protected-access 740 try: 741 namespace, unknown_args = ( 742 deepest_parser._remainder_action.ParseRemainingArgs( 743 unknown_args, namespace, args)) 744 # There still may be unknown_args that came before the last known arg. 745 if not unknown_args: 746 return namespace 747 except parser_errors.UnrecognizedArgumentsError: 748 # In the case of UnrecognizedArgumentsError, we want to just let it 749 # continue so that we can get the nicer error handling. 750 pass 751 752 deepest_parser._Suggest(unknown_args) 753 754 def _check_value(self, action, value): 755 """Overrides argparse.ArgumentParser's ._check_value(action, value) method. 756 757 Args: 758 action: argparse.Action, The action being checked against this value. 759 value: The parsed command line argument provided that needs to correspond 760 to this action. 761 762 Raises: 763 argparse.ArgumentError: If the action and value don't work together. 764 """ 765 is_subparser = isinstance(action, CloudSDKSubParsersAction) 766 767 # When using tab completion, argcomplete monkey patches various parts of 768 # argparse and interferes with the normal argument parsing flow. Here, we 769 # need to set self._orig_class because argcomplete compares this 770 # directly to argparse._SubParsersAction to see if it should recursively 771 # patch this parser. It should really check to see if it is a subclass 772 # but alas, it does not. If we don't set this, argcomplete will not patch, 773 # our subparser and completions below this point won't work. Normally we 774 # would just set this in action.IsValidChoice() but sometimes this 775 # sub-element has already been loaded and is already in action.choices. In 776 # either case, we still need argcomplete to patch this subparser so it 777 # can compute completions below this point. 778 if is_subparser and '_ARGCOMPLETE' in os.environ: 779 # pylint:disable=protected-access, Required by argcomplete. 780 action._orig_class = argparse._SubParsersAction 781 # This is copied from this method in argparse's version of this method. 782 if action.choices is None or value in action.choices: 783 return 784 if isinstance(value, six.string_types): 785 arg = value 786 else: 787 arg = six.text_type(value) 788 789 # We add this to check if we can lazy load the element. 790 if is_subparser and action.IsValidChoice(arg): 791 return 792 793 # Not something we know, raise an error. 794 # pylint:disable=protected-access 795 cli_generator = self._calliope_command._cli_generator 796 missing_components = cli_generator.ComponentsForMissingCommand( 797 self._calliope_command.GetPath() + [arg]) 798 if missing_components: 799 msg = ('You do not currently have this command group installed. Using ' 800 'it requires the installation of components: ' 801 '[{missing_components}]'.format( 802 missing_components=', '.join(missing_components))) 803 update_manager.UpdateManager.EnsureInstalledAndRestart( 804 missing_components, msg=msg) 805 806 if is_subparser: 807 # We are going to show the usage anyway, which requires loading 808 # everything. Do this here so that choices gets populated. 809 action.LoadAllChoices() 810 811 # Command is not valid, see what we can suggest as a fix... 812 message = "Invalid choice: '{0}'.".format(value) 813 814 # Determine if the requested command is available in another release track. 815 existing_alternatives = self._ExistingCommandAlternativeReleaseTracks(arg) 816 if existing_alternatives: 817 message += ('\nThis command is available in one or more alternate ' 818 'release tracks. Try:\n ') 819 message += '\n '.join(existing_alternatives) 820 821 # Log to analytics the attempt to execute a command. 822 # We know the user entered 'value' is a valid command in a different 823 # release track. It's safe to include it. 824 self._Error(parser_errors.WrongTrackError( 825 message, 826 parser=self, 827 extra_path_arg=arg, 828 suggestions=existing_alternatives)) 829 830 # If we are dealing with flags, see if the spelling was close to something 831 # else that exists here. 832 suggestion = None 833 choices = sorted(action.choices) 834 if not is_subparser: 835 suggester = usage_text.TextChoiceSuggester(choices) 836 suggestion = suggester.GetSuggestion(arg) 837 if suggestion: 838 message += " Did you mean '{0}'?".format(suggestion) 839 else: 840 # Command group choices will be displayed in the usage message. 841 message += '\n\nValid choices are [{0}].'.format( 842 ', '.join([six.text_type(c) for c in choices])) 843 844 # Log to analytics the attempt to execute a command. 845 # We don't know if the user entered 'value' is a mistyped command or 846 # some resource name that the user entered and we incorrectly thought it's 847 # a command. We can't include it since it might be PII. 848 849 self._Error(parser_errors.UnknownCommandError( 850 message, 851 argument=action.option_strings[0] if action.option_strings else None, 852 total_unrecognized=1, 853 total_suggestions=1 if suggestion else 0, 854 suggestions=[suggestion] if suggestion else choices)) 855 856 def _CommandAlternativeReleaseTracks(self, value=None): 857 """Gets alternatives for the command in other release tracks. 858 859 Args: 860 value: str, The value being parsed. 861 862 Returns: 863 [CommandCommon]: The alternatives for the command in other release tracks. 864 """ 865 existing_alternatives = [] 866 # pylint:disable=protected-access 867 cli_generator = self._calliope_command._cli_generator 868 alternates = cli_generator.ReplicateCommandPathForAllOtherTracks( 869 self._calliope_command.GetPath() + ([value] if value else [])) 870 if alternates: 871 top_element = self._calliope_command._TopCLIElement() 872 for _, command_path in sorted(six.iteritems(alternates), 873 key=lambda x: x[0].prefix or ''): 874 alternative_cmd = top_element.LoadSubElementByPath(command_path[1:]) 875 if alternative_cmd and not alternative_cmd.IsHidden(): 876 existing_alternatives.append(alternative_cmd) 877 return existing_alternatives 878 879 def _ExistingFlagAlternativeReleaseTracks(self, arg): 880 """Checks whether the arg exists in other tracks of the command. 881 882 Args: 883 arg: str, The argument being parsed. 884 885 Returns: 886 [str]: The names of alternate commands that the user may use. 887 """ 888 res = [] 889 for alternate in self._CommandAlternativeReleaseTracks(): 890 if arg in [f.option_strings[0] for f in alternate.GetAllAvailableFlags()]: 891 res.append(' '.join(alternate.GetPath()) + ' ' + arg) 892 return res 893 894 def _ExistingCommandAlternativeReleaseTracks(self, value): 895 """Gets the path of alternatives for the command in other release tracks. 896 897 Args: 898 value: str, The value being parsed. 899 900 Returns: 901 [str]: The names of alternate commands that the user may use. 902 """ 903 return [' '.join(alternate.GetPath()) for alternate in 904 self._CommandAlternativeReleaseTracks(value=value)] 905 906 def _ReportErrorMetricsHelper(self, dotted_command_path, error, 907 error_extra_info=None): 908 """Logs `Commands` and `Error` Google Analytics events for an error. 909 910 Args: 911 dotted_command_path: str, The dotted path to as much of the command as we 912 can identify before an error. Example: gcloud.projects 913 error: class, The class (not the instance) of the Exception for an error. 914 error_extra_info: {str: json-serializable}, A json serializable dict of 915 extra info that we want to log with the error. This enables us to write 916 queries that can understand the keys and values in this dict. 917 """ 918 specified_args = self.GetSpecifiedArgNames() 919 metrics.Commands( 920 dotted_command_path, 921 config.CLOUD_SDK_VERSION, 922 specified_args, 923 error=error, 924 error_extra_info=error_extra_info) 925 metrics.Error( 926 dotted_command_path, 927 error, 928 specified_args, 929 error_extra_info=error_extra_info) 930 931 def ReportErrorMetrics(self, error, message): 932 """Reports Command and Error metrics in case of argparse errors. 933 934 Args: 935 error: Exception, The Exception object. 936 message: str, The exception error message. 937 """ 938 dotted_command_path = '.'.join(self._calliope_command.GetPath()) 939 940 # Check for parser_errors.ArgumentError with metrics payload. 941 if isinstance(error, parser_errors.ArgumentError): 942 if error.extra_path_arg: 943 dotted_command_path = '.'.join([dotted_command_path, 944 error.extra_path_arg]) 945 self._ReportErrorMetricsHelper(dotted_command_path, 946 error.__class__, 947 error.error_extra_info) 948 return 949 950 # No specific exception with metrics, try to detect error from message. 951 if 'too few arguments' in message: 952 self._ReportErrorMetricsHelper(dotted_command_path, 953 parser_errors.TooFewArgumentsError) 954 return 955 956 # Catchall for any error we didn't explicitly detect. 957 self._ReportErrorMetricsHelper(dotted_command_path, 958 parser_errors.OtherParsingError) 959 960 def error(self, message='', context=None, reproduce=False): 961 """Overrides argparse.ArgumentParser's .error(message) method. 962 963 Specifically, it avoids reprinting the program name and the string 964 "error:". 965 966 Args: 967 message: str, The error message to print. 968 context: _ErrorContext, An error context with affected parser. 969 reproduce: bool, Reproduce a previous call to this method from context. 970 """ 971 if reproduce and context: 972 # Reproduce a previous call to this method from the info in context. 973 message = context.message 974 parser = context.parser 975 error = context.error 976 if not error: 977 error = parser_errors.ArgumentError(message, parser=self) 978 else: 979 if context: 980 message = context.message 981 parser = context.parser 982 error = context.error 983 else: 984 if 'Invalid choice:' in message: 985 exc = parser_errors.UnrecognizedArgumentsError 986 else: 987 exc = parser_errors.ArgumentError 988 if message: 989 message = re.sub(r"\bu'", "'", message) 990 error = exc(message, parser=self) 991 parser = self 992 if ('_ARGCOMPLETE' not in os.environ and 993 not isinstance(error, parser_errors.DetailedArgumentError) and 994 ( 995 self._probe_error or 996 'Invalid choice' in message or 997 'unknown parser' in message 998 ) 999 ): 1000 if 'unknown parser' in message: 1001 return 1002 if self._probe_error and 'expected one argument' in message: 1003 return 1004 # Save this context for later. We may be able to deduce a better error 1005 # message. For instance, argparse might complain about an invalid 1006 # command choice 'flag-value' for '--unknown-flag flag-value', but 1007 # with a little finagling in parse_known_args() we can verify that 1008 # '--unknown-flag' is in fact an unknown flag and error out on that. 1009 self._SetErrorContext(context or _ErrorContext(message, parser, error)) 1010 return 1011 1012 # Add file/line info if specified. 1013 1014 prefix = 'argument ' 1015 if context and message.startswith(prefix): 1016 parts = message.split(':', 1) 1017 arg = context.AddLocations(parts[0][len(prefix):]) 1018 message = '{}{}:{}'.format(prefix, arg, parts[1]) 1019 1020 # Ignore errors better handled by validate_specified_args(). 1021 if '_ARGCOMPLETE' not in os.environ: 1022 if re.search('too few arguments', message): 1023 return 1024 if (re.search('arguments? .* required', message) and 1025 not re.search('in dict arg but not provided', message) and 1026 not re.search(r'\[.*\brequired\b.*\]', message)): 1027 return 1028 1029 # No need to output help/usage text if we are in completion mode. However, 1030 # we do need to populate group/command level choices. These choices are not 1031 # loaded when there is a parser error since we do lazy loading. 1032 if '_ARGCOMPLETE' in os.environ: 1033 # pylint:disable=protected-access 1034 if self._calliope_command._sub_parser: 1035 self._calliope_command.LoadAllSubElements() 1036 else: 1037 message = console_attr.SafeText(message) 1038 log.error('({prog}) {message}'.format(prog=self.prog, message=message)) 1039 # multi-line message means hints already added, no need for usage. 1040 # pylint: disable=protected-access 1041 if '\n' not in message: 1042 # Provide "Maybe you meant" suggestions if we are dealing with an 1043 # invalid command. 1044 suggestions = None 1045 if 'Invalid choice' in message: 1046 suggestions = suggest_commands.GetCommandSuggestions( 1047 self._GetOriginalArgs()) 1048 self._ClearOriginalArgs() 1049 if suggestions: 1050 argparse._sys.stderr.write( 1051 '\n '.join(['Maybe you meant:'] + suggestions) + '\n') 1052 argparse._sys.stderr.write('\n' + _HELP_SEARCH_HINT + '\n') 1053 error.error_extra_info = { 1054 'suggestions': suggestions, 1055 'total_suggestions': len(suggestions), 1056 'total_unrecognized': 1, 1057 } 1058 # Otherwise print out usage string. 1059 elif 'Command name argument expected.' == message: 1060 usage_string = self._calliope_command.GetCategoricalUsage() 1061 # The next if clause is executed if there were no categories to 1062 # display. 1063 uncategorized_usage = False 1064 if not usage_string: 1065 uncategorized_usage = True 1066 usage_string = self._calliope_command.GetUncategorizedUsage() 1067 interactive = False 1068 if not uncategorized_usage: 1069 interactive = console_io.IsInteractive(error=True) 1070 if interactive: 1071 out = io.StringIO() 1072 out.write('{message}\n'.format(message=message)) 1073 else: 1074 out = argparse._sys.stderr 1075 out.write('\n') 1076 render_document.RenderDocument( 1077 fin=io.StringIO(usage_string), out=out) 1078 if uncategorized_usage: 1079 out.write(self._calliope_command.GetHelpHint()) 1080 if interactive: 1081 console_io.More(out.getvalue(), out=argparse._sys.stderr) 1082 else: 1083 usage_string = self._calliope_command.GetUsage() 1084 argparse._sys.stderr.write(usage_string) 1085 1086 parser.ReportErrorMetrics(error, message) 1087 self.exit(2, exception=error) 1088 1089 def exit(self, status=0, message=None, exception=None): 1090 """Overrides argparse.ArgumentParser's .exit() method. 1091 1092 Args: 1093 status: int, The exit status. 1094 message: str, The error message to print. 1095 exception: Exception, The exception that caused the exit, if any. 1096 """ 1097 del message # self.error() handles all messaging 1098 del exception # checked by the test harness to differentiate exit causes 1099 sys.exit(status) 1100 1101 def _parse_optional(self, arg_string): 1102 """Overrides argparse.ArgumentParser's ._parse_optional method. 1103 1104 This allows the parser to have leading flags included in the grabbed 1105 arguments and stored in the namespace. 1106 1107 Args: 1108 arg_string: str, The argument string. 1109 1110 Returns: 1111 The normal return value of argparse.ArgumentParser._parse_optional. 1112 """ 1113 if not isinstance(arg_string, six.string_types): 1114 # Flag value injected by --flags-file. 1115 return None 1116 positional_actions = self._get_positional_actions() 1117 option_tuple = super(ArgumentParser, self)._parse_optional(arg_string) 1118 # If parse_optional finds an action for this arg_string, use that option. 1119 # Note: option_tuple = (action, option_string, explicit_arg) or None 1120 known_option = option_tuple and option_tuple[0] 1121 if (len(positional_actions) == 1 and 1122 positional_actions[0].nargs == argparse.REMAINDER and 1123 not known_option): 1124 return None 1125 return option_tuple 1126 1127 def _get_values(self, action, arg_strings): 1128 """Intercepts argparse.ArgumentParser's ._get_values method. 1129 1130 This intercept does not actually change any behavior. We use this hook to 1131 grab the flags and arguments that are actually seen at parse time. The 1132 resulting namespace has entries for every argument (some with defaults) so 1133 we can't know which the user actually typed. 1134 1135 Args: 1136 action: Action, the action that is being processed. 1137 arg_strings: [str], The values provided for this action. 1138 1139 Returns: 1140 Whatever the parent method returns. 1141 """ 1142 if action.dest != argparse.SUPPRESS: # argparse SUPPRESS usage 1143 # Don't look at the action unless it is a real argument or flag. The 1144 # suppressed destination indicates that it is a SubParsers action. 1145 name = None 1146 if action.option_strings: 1147 # This is a flag, save the first declared name of the flag. 1148 name = action.option_strings[0] 1149 elif arg_strings: 1150 # This is a positional and there are arguments to consume. Optional 1151 # positionals will always get to this method, so we need to ignore the 1152 # ones for which a value was not actually provided. If it is provided, 1153 # save the metavar name or the destination name. 1154 name = action.metavar if action.metavar else action.dest 1155 if action.nargs and action.nargs != '?': 1156 # This arg takes in multiple values, record how many were provided. 1157 # (? means 0 or 1, so treat that as an arg that takes a single value. 1158 name += ':' + six.text_type(len(arg_strings)) 1159 if name: 1160 self._specified_args[action.dest] = name 1161 return super(ArgumentParser, self)._get_values(action, arg_strings) 1162 1163 def _get_option_tuples(self, option_string): 1164 """Intercepts argparse.ArgumentParser's ._get_option_tuples method. 1165 1166 Cloud SDK no longer supports flag abbreviations, so it always returns [] 1167 for the non-arg-completion case to indicate no abbreviated flag matches. 1168 1169 Args: 1170 option_string: The option string to match. 1171 1172 Returns: 1173 A list of matching flag tuples. 1174 """ 1175 if '_ARGCOMPLETE' in os.environ: 1176 return super(ArgumentParser, self)._get_option_tuples(option_string) 1177 return [] # This effectively disables abbreviations. 1178 1179 1180# pylint:disable=protected-access 1181class CloudSDKSubParsersAction(six.with_metaclass(abc.ABCMeta, 1182 argparse._SubParsersAction)): 1183 """A custom subclass for arg parsing behavior. 1184 1185 While the above ArgumentParser overrides behavior for parsing the flags 1186 associated with a specific group or command, this class overrides behavior 1187 for loading those sub parsers. 1188 """ 1189 1190 @abc.abstractmethod 1191 def IsValidChoice(self, choice): 1192 """Determines if the given arg is a valid sub group or command. 1193 1194 Args: 1195 choice: str, The name of the sub element to check. 1196 1197 Returns: 1198 bool, True if the given item is a valid sub element, False otherwise. 1199 """ 1200 pass 1201 1202 @abc.abstractmethod 1203 def LoadAllChoices(self): 1204 """Load all the choices because we need to know the full set.""" 1205 pass 1206 1207 1208class CommandGroupAction(CloudSDKSubParsersAction): 1209 """A subparser for loading calliope command groups on demand. 1210 1211 We use this to intercept the parsing right before it needs to start parsing 1212 args for sub groups and we then load the specific sub group it needs. 1213 """ 1214 1215 def __init__(self, *args, **kwargs): 1216 self._calliope_command = kwargs.pop('calliope_command') 1217 super(CommandGroupAction, self).__init__(*args, **kwargs) 1218 1219 def IsValidChoice(self, choice): 1220 # When using tab completion, argcomplete monkey patches various parts of 1221 # argparse and interferes with the normal argument parsing flow. Usually 1222 # it is sufficient to check if the given choice is valid here, but delay 1223 # the loading until __call__ is invoked later during the parsing process. 1224 # During completion time, argcomplete tries to patch the subparser before 1225 # __call__ is called, so nothing has been loaded yet. We need to force 1226 # load things here so that there will be something loaded for it to patch. 1227 if '_ARGCOMPLETE' in os.environ: 1228 self._calliope_command.LoadSubElement(choice) 1229 return self._calliope_command.IsValidSubElement(choice) 1230 1231 def LoadAllChoices(self): 1232 self._calliope_command.LoadAllSubElements() 1233 1234 def __call__(self, parser, namespace, values, option_string=None): 1235 # This is the name of the arg that is the sub element that needs to be 1236 # loaded. 1237 parser_name = values[0] 1238 # Load that element if it's there. If it's not valid, nothing will be 1239 # loaded and normal error handling will take over. 1240 if self._calliope_command: 1241 self._calliope_command.LoadSubElement(parser_name) 1242 super(CommandGroupAction, self).__call__( 1243 parser, namespace, values, option_string=option_string) 1244 1245 1246class DynamicPositionalAction(six.with_metaclass(abc.ABCMeta, 1247 CloudSDKSubParsersAction)): 1248 """An argparse action that adds new flags to the parser when it is called. 1249 1250 We need to use a subparser for this because for a given parser, argparse 1251 collects all the arg information before it starts parsing. Adding in new flags 1252 on the fly doesn't work. With a subparser, it is independent so we can load 1253 flags into here on the fly before argparse loads this particular parser. 1254 """ 1255 1256 def __init__(self, *args, **kwargs): 1257 self.hidden = kwargs.pop('hidden', False) 1258 self._parent_ai = kwargs.pop('parent_ai') 1259 super(DynamicPositionalAction, self).__init__(*args, **kwargs) 1260 1261 def IsValidChoice(self, choice): 1262 # We need to actually create the parser or else check_value will fail if the 1263 # given choice is not present. We just add it no matter what it is because 1264 # we don't have access to the namespace to be able to figure out if the 1265 # choice is actually valid. Invalid choices will raise exceptions once 1266 # called. We also don't actually care what the values are in here because we 1267 # register an explicit completer to use for completions, so the list of 1268 # parsers is not actually used other than to bypass the check_value 1269 # validation. 1270 self._AddParser(choice) 1271 # By default, don't do any checking of the argument. If it is bad, raise 1272 # an exception when it is called. We don't need to do any on-demand loading 1273 # here because there are no subparsers of this one, so the above argcomplete 1274 # issue doesn't matter. 1275 return True 1276 1277 def LoadAllChoices(self): 1278 # We don't need to do this because we will use an explicit completer to 1279 # complete the names of the options rather than relying on correctly 1280 # populating the choices. 1281 pass 1282 1283 def _AddParser(self, choice): 1284 # Create a new parser and pass in the calliope_command of the original so 1285 # that things like help and error reporting continue to work. 1286 return self.add_parser( 1287 choice, add_help=False, prog=self._parent_ai.parser.prog, 1288 calliope_command=self._parent_ai.parser._calliope_command) 1289 1290 @abc.abstractmethod 1291 def GenerateArgs(self, namespace, choice): 1292 pass 1293 1294 @abc.abstractmethod 1295 def Completions(self, prefix, parsed_args, **kwargs): 1296 pass 1297 1298 def __call__(self, parser, namespace, values, option_string=None): 1299 choice = values[0] 1300 args = self.GenerateArgs(namespace, choice) 1301 sub_parser = self._name_parser_map[choice] 1302 1303 # This is tricky. When we create a new parser above, that parser does not 1304 # have any of the flags from the parent command. We need to propagate them 1305 # all down to this parser like we do in calliope. We also want to add new 1306 # flags. In order for those to show up in the help, they need to be 1307 # registered with an ArgumentInterceptor. Here, we create one and seed it 1308 # with the data of the parent. This actually means that every flag we add 1309 # to our new parser will show up in the help of the parent parser, even 1310 # though those flags are not actually on that parser. This is ok because 1311 # help is always run on the parent ArgumentInterceptor and we want it to 1312 # show the full set of args. 1313 ai = parser_arguments.ArgumentInterceptor( 1314 sub_parser, is_global=False, cli_generator=None, 1315 allow_positional=True, data=self._parent_ai.data) 1316 1317 for flag in itertools.chain(self._parent_ai.flag_args, 1318 self._parent_ai.ancestor_flag_args): 1319 # Propagate the flags down except the ones we are not supposed to. Note 1320 # that we *do* copy the help action unlike we usually do because this 1321 # subparser is going to share the help action of the parent. 1322 if flag.do_not_propagate or flag.is_required: 1323 continue 1324 # We add the flags directly to the parser instead of the 1325 # ArgumentInterceptor because if we didn't the flags would be duplicated 1326 # in the help, since we reused the data object from the parent. 1327 sub_parser._add_action(flag) 1328 # Update parent display_info in children, children take precedence. 1329 ai.display_info.AddLowerDisplayInfo(self._parent_ai.display_info) 1330 1331 # Add args to the parser and remove any collisions if arguments are 1332 # already registered with the same name. 1333 for arg in args: 1334 arg.RemoveFromParser(ai) 1335 added_arg = arg.AddToParser(ai) 1336 # Argcomplete patches parsers and actions before call() is called. Since 1337 # we generate these args at call() time, they have not been patched and 1338 # causes completion to fail. Since we know that we are not going to be 1339 # adding any subparsers (the only thing that actually needs to be patched) 1340 # we fake it here to make argcomplete think it did the patching so it 1341 # doesn't crash. 1342 if '_ARGCOMPLETE' in os.environ and not hasattr(added_arg, '_orig_class'): 1343 added_arg._orig_class = added_arg.__class__ 1344 1345 super(DynamicPositionalAction, self).__call__( 1346 parser, namespace, values, option_string=option_string) 1347 1348 # Running two dynamic commands in a row using the same CLI object is a 1349 # problem because the argparse parsers are saved in between invocations. 1350 # This is usually fine because everything is static, but in this case two 1351 # invocations could actually have different dynamic args generated. We 1352 # have to do two things to get this to work. First we need to clear the 1353 # parser from the map. If we don't do this, this class doesn't even get 1354 # called again because the choices are already defined. Second, we need 1355 # to remove the arguments we added from the ArgumentInterceptor. The 1356 # parser itself is thrown out, but because we are sharing an 1357 # ArgumentInterceptor with our parent, it remembers the args that we 1358 # added. Later, they are propagated back down to us even though they no 1359 # longer actually exist. When completing, we know we will only be running 1360 # a single invocation and we need to leave the choices around so that the 1361 # completer can read them after the command fails to run. 1362 if '_ARGCOMPLETE' not in os.environ: 1363 self._name_parser_map.clear() 1364 # Detaching the argument interceptors here makes the help text work by 1365 # preventing the accumlation of duplicate entries with each command 1366 # execution on this CLI. However, it also foils the ability to map arg 1367 # dest names back to the original argument, needed for the flag completion 1368 # style. It's commented out here just in case help text wins out over 1369 # argument lookup down the road. 1370 # for _, arg in args.iteritems(): 1371 # arg.RemoveFromParser(ai) 1372