File: //snap/google-cloud-cli/current/lib/googlecloudsdk/command_lib/interactive/parser.py
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A basic command line parser.
This command line parser does the bare minimum required to understand the
commands and flags being used as well as perform completion. This is not a
replacement for argparse (yet).
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
from googlecloudsdk.calliope import cli_tree
from googlecloudsdk.command_lib.interactive import lexer
import six
LOOKUP_ATTR = cli_tree.LOOKUP_ATTR
LOOKUP_COMMANDS = cli_tree.LOOKUP_COMMANDS
LOOKUP_CHOICES = cli_tree.LOOKUP_CHOICES
LOOKUP_HIDDEN_CHOICES = cli_tree.LOOKUP_HIDDEN_CHOICES
LOOKUP_COMPLETER = cli_tree.LOOKUP_COMPLETER
LOOKUP_FLAGS = cli_tree.LOOKUP_FLAGS
LOOKUP_GROUPS = cli_tree.LOOKUP_GROUPS
LOOKUP_IS_GROUP = cli_tree.LOOKUP_IS_GROUP
LOOKUP_IS_HIDDEN = cli_tree.LOOKUP_IS_HIDDEN
LOOKUP_IS_SPECIAL = 'interactive.is_special'
LOOKUP_NAME = cli_tree.LOOKUP_NAME
LOOKUP_NARGS = cli_tree.LOOKUP_NARGS
LOOKUP_POSITIONALS = cli_tree.LOOKUP_POSITIONALS
LOOKUP_TYPE = cli_tree.LOOKUP_TYPE
LOOKUP_CLI_VERSION = cli_tree.LOOKUP_CLI_VERSION
class ArgTokenType(enum.Enum):
UNKNOWN = 0 # Unknown token type in any position
PREFIX = 1 # Potential command name, maybe after lex.SHELL_TERMINATOR_CHARS
GROUP = 2 # Command arg with subcommands
COMMAND = 3 # Command arg
FLAG = 4 # Flag arg
FLAG_ARG = 5 # Flag value arg
POSITIONAL = 6 # Positional arg
SPECIAL = 7 # Special keyword that is followed by PREFIX.
class ArgToken(object):
"""Shell token info.
Attributes:
value: A string associated with the token.
token_type: Instance of ArgTokenType
tree: A subtree of CLI root.
start: The index of the first char in the original string.
end: The index directly after the last char in the original string.
"""
def __init__(self, value, token_type=ArgTokenType.UNKNOWN, tree=None,
start=None, end=None):
self.value = value
self.token_type = token_type
self.tree = tree
self.start = start
self.end = end
def __eq__(self, other):
"""Equality based on properties."""
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
return False
def __repr__(self):
"""Improve debugging during tests."""
return 'ArgToken({}, {}, {}, {})'.format(self.value, self.token_type,
self.start, self.end)
class Parser(object):
"""Shell command line parser.
Attributes:
args:
context:
cmd:
hidden:
positionals_seen:
root:
statement:
tokens:
"""
def __init__(self, root, context=None, hidden=False):
self.root = root
self.hidden = hidden
self.args = []
self.cmd = self.root
self.positionals_seen = 0
self.previous_line = None
self.statement = 0
self.tokens = None
self.SetContext(context)
def SetContext(self, context=None):
"""Sets the default command prompt context."""
self.context = six.text_type(context or '')
def ParseCommand(self, line):
"""Parses the next command from line and returns a list of ArgTokens.
The parse stops at the first token that is not an ARG or FLAG. That token is
not consumed. The caller can examine the return value to determine the
parts of the line that were ignored and the remainder of the line that was
not lexed/parsed yet.
Args:
line: a string containing the current command line
Returns:
A list of ArgTokens.
"""
self.tokens = lexer.GetShellTokens(line)
self.cmd = self.root
self.positionals_seen = 0
self.args = []
unknown = False
while self.tokens:
token = self.tokens.pop(0)
value = token.UnquotedValue()
if token.lex == lexer.ShellTokenType.TERMINATOR:
unknown = False
self.cmd = self.root
self.args.append(ArgToken(value, ArgTokenType.SPECIAL, self.cmd,
token.start, token.end))
elif token.lex == lexer.ShellTokenType.FLAG:
self.ParseFlag(token, value)
elif token.lex == lexer.ShellTokenType.ARG and not unknown:
if value in self.cmd[LOOKUP_COMMANDS]:
self.cmd = self.cmd[LOOKUP_COMMANDS][value]
if self.cmd[LOOKUP_IS_GROUP]:
token_type = ArgTokenType.GROUP
elif LOOKUP_IS_SPECIAL in self.cmd:
token_type = ArgTokenType.SPECIAL
self.cmd = self.root
else:
token_type = ArgTokenType.COMMAND
self.args.append(ArgToken(value, token_type, self.cmd,
token.start, token.end))
elif self.cmd == self.root and '=' in value:
token_type = ArgTokenType.SPECIAL
self.cmd = self.root
self.args.append(ArgToken(value, token_type, self.cmd,
token.start, token.end))
elif self.positionals_seen < len(self.cmd[LOOKUP_POSITIONALS]):
positional = self.cmd[LOOKUP_POSITIONALS][self.positionals_seen]
self.args.append(ArgToken(value, ArgTokenType.POSITIONAL,
positional, token.start, token.end))
if positional[LOOKUP_NARGS] not in ('*', '+'):
self.positionals_seen += 1
elif not value: # trailing space
break
else:
unknown = True
if self.cmd == self.root:
token_type = ArgTokenType.PREFIX
else:
token_type = ArgTokenType.UNKNOWN
self.args.append(ArgToken(value, token_type, self.cmd,
token.start, token.end))
else:
unknown = True
self.args.append(ArgToken(value, ArgTokenType.UNKNOWN, self.cmd,
token.start, token.end))
return self.args
def ParseFlag(self, token, name):
"""Parses the flag token and appends it to the arg list."""
name_start = token.start
name_end = token.end
value = None
value_start = None
value_end = None
if '=' in name:
# inline flag value
name, value = name.split('=', 1)
name_end = name_start + len(name)
value_start = name_end + 1
value_end = value_start + len(value)
flag = self.cmd[LOOKUP_FLAGS].get(name)
if not flag or not self.hidden and flag[LOOKUP_IS_HIDDEN]:
self.args.append(ArgToken(name, ArgTokenType.UNKNOWN, self.cmd,
token.start, token.end))
return
if flag[LOOKUP_TYPE] != 'bool' and value is None and self.tokens:
# next arg is the flag value
token = self.tokens.pop(0)
value = token.UnquotedValue()
value_start = token.start
value_end = token.end
self.args.append(ArgToken(name, ArgTokenType.FLAG, flag,
name_start, name_end))
if value is not None:
self.args.append(ArgToken(value, ArgTokenType.FLAG_ARG, None,
value_start, value_end))