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