1# -*- coding: utf-8 -*- #
2# Copyright 2017 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"""A basic command line parser.
17
18This command line parser does the bare minimum required to understand the
19commands and flags being used as well as perform completion. This is not a
20replacement for argparse (yet).
21"""
22
23from __future__ import absolute_import
24from __future__ import division
25from __future__ import unicode_literals
26
27import enum
28
29from googlecloudsdk.calliope import cli_tree
30from googlecloudsdk.command_lib.interactive import lexer
31
32import six
33
34
35LOOKUP_COMMANDS = cli_tree.LOOKUP_COMMANDS
36LOOKUP_CHOICES = cli_tree.LOOKUP_CHOICES
37LOOKUP_COMPLETER = cli_tree.LOOKUP_COMPLETER
38LOOKUP_FLAGS = cli_tree.LOOKUP_FLAGS
39LOOKUP_GROUPS = cli_tree.LOOKUP_GROUPS
40LOOKUP_IS_GROUP = cli_tree.LOOKUP_IS_GROUP
41LOOKUP_IS_HIDDEN = cli_tree.LOOKUP_IS_HIDDEN
42LOOKUP_IS_SPECIAL = 'interactive.is_special'
43LOOKUP_NAME = cli_tree.LOOKUP_NAME
44LOOKUP_NARGS = cli_tree.LOOKUP_NARGS
45LOOKUP_POSITIONALS = cli_tree.LOOKUP_POSITIONALS
46LOOKUP_TYPE = cli_tree.LOOKUP_TYPE
47
48LOOKUP_CLI_VERSION = cli_tree.LOOKUP_CLI_VERSION
49
50
51class ArgTokenType(enum.Enum):
52  UNKNOWN = 0  # Unknown token type in any position
53  PREFIX = 1  # Potential command name, maybe after lex.SHELL_TERMINATOR_CHARS
54  GROUP = 2  # Command arg with subcommands
55  COMMAND = 3  # Command arg
56  FLAG = 4  # Flag arg
57  FLAG_ARG = 5  # Flag value arg
58  POSITIONAL = 6  # Positional arg
59  SPECIAL = 7  # Special keyword that is followed by PREFIX.
60
61
62class ArgToken(object):
63  """Shell token info.
64
65  Attributes:
66    value: A string associated with the token.
67    token_type: Instance of ArgTokenType
68    tree: A subtree of CLI root.
69    start: The index of the first char in the original string.
70    end: The index directly after the last char in the original string.
71  """
72
73  def __init__(self, value, token_type=ArgTokenType.UNKNOWN, tree=None,
74               start=None, end=None):
75    self.value = value
76    self.token_type = token_type
77    self.tree = tree
78    self.start = start
79    self.end = end
80
81  def __eq__(self, other):
82    """Equality based on properties."""
83    if isinstance(other, self.__class__):
84      return self.__dict__ == other.__dict__
85    return False
86
87  def __repr__(self):
88    """Improve debugging during tests."""
89    return 'ArgToken({}, {}, {}, {})'.format(self.value, self.token_type,
90                                             self.start, self.end)
91
92
93class Parser(object):
94  """Shell command line parser.
95
96  Attributes:
97    args:
98    context:
99    cmd:
100    hidden:
101    positionals_seen:
102    root:
103    statement:
104    tokens:
105  """
106
107  def __init__(self, root, context=None, hidden=False):
108    self.root = root
109    self.hidden = hidden
110
111    self.args = []
112    self.cmd = self.root
113    self.positionals_seen = 0
114    self.previous_line = None
115    self.statement = 0
116    self.tokens = None
117
118    self.SetContext(context)
119
120  def SetContext(self, context=None):
121    """Sets the default command prompt context."""
122    self.context = six.text_type(context or '')
123
124  def ParseCommand(self, line):
125    """Parses the next command from line and returns a list of ArgTokens.
126
127    The parse stops at the first token that is not an ARG or FLAG. That token is
128    not consumed. The caller can examine the return value to determine the
129    parts of the line that were ignored and the remainder of the line that was
130    not lexed/parsed yet.
131
132    Args:
133      line: a string containing the current command line
134
135    Returns:
136      A list of ArgTokens.
137    """
138    self.tokens = lexer.GetShellTokens(line)
139    self.cmd = self.root
140    self.positionals_seen = 0
141
142    self.args = []
143
144    unknown = False
145    while self.tokens:
146      token = self.tokens.pop(0)
147      value = token.UnquotedValue()
148
149      if token.lex == lexer.ShellTokenType.TERMINATOR:
150        unknown = False
151        self.cmd = self.root
152        self.args.append(ArgToken(value, ArgTokenType.SPECIAL, self.cmd,
153                                  token.start, token.end))
154
155      elif token.lex == lexer.ShellTokenType.FLAG:
156        self.ParseFlag(token, value)
157
158      elif token.lex == lexer.ShellTokenType.ARG and not unknown:
159        if value in self.cmd[LOOKUP_COMMANDS]:
160          self.cmd = self.cmd[LOOKUP_COMMANDS][value]
161          if self.cmd[LOOKUP_IS_GROUP]:
162            token_type = ArgTokenType.GROUP
163          elif LOOKUP_IS_SPECIAL in self.cmd:
164            token_type = ArgTokenType.SPECIAL
165            self.cmd = self.root
166          else:
167            token_type = ArgTokenType.COMMAND
168          self.args.append(ArgToken(value, token_type, self.cmd,
169                                    token.start, token.end))
170
171        elif self.cmd == self.root and '=' in value:
172          token_type = ArgTokenType.SPECIAL
173          self.cmd = self.root
174          self.args.append(ArgToken(value, token_type, self.cmd,
175                                    token.start, token.end))
176
177        elif self.positionals_seen < len(self.cmd[LOOKUP_POSITIONALS]):
178          positional = self.cmd[LOOKUP_POSITIONALS][self.positionals_seen]
179          self.args.append(ArgToken(value, ArgTokenType.POSITIONAL,
180                                    positional, token.start, token.end))
181          if positional[LOOKUP_NARGS] not in ('*', '+'):
182            self.positionals_seen += 1
183
184        elif not value:  # trailing space
185          break
186
187        else:
188          unknown = True
189          if self.cmd == self.root:
190            token_type = ArgTokenType.PREFIX
191          else:
192            token_type = ArgTokenType.UNKNOWN
193          self.args.append(ArgToken(value, token_type, self.cmd,
194                                    token.start, token.end))
195
196      else:
197        unknown = True
198        self.args.append(ArgToken(value, ArgTokenType.UNKNOWN, self.cmd,
199                                  token.start, token.end))
200
201    return self.args
202
203  def ParseFlag(self, token, name):
204    """Parses the flag token and appends it to the arg list."""
205
206    name_start = token.start
207    name_end = token.end
208    value = None
209    value_start = None
210    value_end = None
211
212    if '=' in name:
213      # inline flag value
214      name, value = name.split('=', 1)
215      name_end = name_start + len(name)
216      value_start = name_end + 1
217      value_end = value_start + len(value)
218
219    flag = self.cmd[LOOKUP_FLAGS].get(name)
220    if not flag or not self.hidden and flag[LOOKUP_IS_HIDDEN]:
221      self.args.append(ArgToken(name, ArgTokenType.UNKNOWN, self.cmd,
222                                token.start, token.end))
223      return
224
225    if flag[LOOKUP_TYPE] != 'bool' and value is None and self.tokens:
226      # next arg is the flag value
227      token = self.tokens.pop(0)
228      value = token.UnquotedValue()
229      value_start = token.start
230      value_end = token.end
231
232    self.args.append(ArgToken(name, ArgTokenType.FLAG, flag,
233                              name_start, name_end))
234    if value is not None:
235      self.args.append(ArgToken(value, ArgTokenType.FLAG_ARG, None,
236                                value_start, value_end))
237