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