268 lines
7.9 KiB
Python
268 lines
7.9 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2016 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.
|
|
|
|
"""Methods for looking up completions from the static CLI tree."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import os
|
|
import shlex
|
|
import sys
|
|
from googlecloudsdk.core.util import encoding
|
|
from googlecloudsdk.core.util import platforms
|
|
import six
|
|
|
|
|
|
LINE_ENV_VAR = 'COMP_LINE'
|
|
POINT_ENV_VAR = 'COMP_POINT'
|
|
IFS_ENV_VAR = '_ARGCOMPLETE_IFS'
|
|
IFS_ENV_DEFAULT = '\013'
|
|
COMPLETIONS_OUTPUT_FD = 8
|
|
|
|
FLAG_PREFIX = '--'
|
|
|
|
FLAG_BOOLEAN = 'bool'
|
|
FLAG_DYNAMIC = 'dynamic'
|
|
FLAG_VALUE = 'value'
|
|
ENV_VAR = 'env_var'
|
|
|
|
LOOKUP_COMMANDS = 'commands'
|
|
LOOKUP_FLAGS = 'flags'
|
|
|
|
_EMPTY_STRING = ''
|
|
_VALUE_SEP = '='
|
|
_SPACE = ' '
|
|
|
|
|
|
class CannotHandleCompletionError(Exception):
|
|
"""Error for when completions cannot be handled."""
|
|
pass
|
|
|
|
|
|
def _GetCmdLineFromEnv():
|
|
"""Gets the command line from the environment.
|
|
|
|
Returns:
|
|
str, Command line.
|
|
"""
|
|
cmd_line = encoding.GetEncodedValue(os.environ, LINE_ENV_VAR)
|
|
completion_point = int(encoding.GetEncodedValue(os.environ, POINT_ENV_VAR))
|
|
cmd_line = cmd_line[:completion_point]
|
|
return cmd_line
|
|
|
|
|
|
def _GetCmdWordQueue(cmd_line):
|
|
"""Converts the given cmd_line to a queue of command line words.
|
|
|
|
Args:
|
|
cmd_line: str, full command line before parsing.
|
|
|
|
Returns:
|
|
[str], Queue of command line words.
|
|
"""
|
|
cmd_words = shlex.split(cmd_line)[1:] # First word should always be 'gcloud'
|
|
|
|
# We need to know if last word was empty. Shlex removes trailing whitespaces.
|
|
if cmd_line[-1] == _SPACE:
|
|
cmd_words.append(_EMPTY_STRING)
|
|
|
|
# Reverse so we can use as a queue
|
|
cmd_words.reverse()
|
|
return cmd_words
|
|
|
|
|
|
def GetEnvVarPrefix():
|
|
# TODO(b/207384119) support powershell environment variables
|
|
return '%' if platforms.OperatingSystem.IsWindows() else '$'
|
|
|
|
|
|
def MatchEnvVars(word, env_vars):
|
|
"""Returns environment variables beginning with `word`.
|
|
|
|
Args:
|
|
word: The word that is being compared to environment variables.
|
|
env_vars: The list of environment variables.
|
|
|
|
Returns:
|
|
[]: No completions.
|
|
[completions]: List, all possible sorted completions.
|
|
"""
|
|
completions = []
|
|
prefix = word[1:] # exclude '$' or '%' and only use the variable name
|
|
for child in env_vars:
|
|
if child.startswith(prefix):
|
|
if platforms.OperatingSystem.IsWindows():
|
|
completions.append('%' + child + '%')
|
|
else:
|
|
completions.append('$' + child)
|
|
return completions
|
|
|
|
|
|
def _FindCompletions(root, cmd_line):
|
|
"""Try to perform a completion based on the static CLI tree.
|
|
|
|
Args:
|
|
root: The root of the tree that will be traversed to find completions.
|
|
cmd_line: [str], original command line.
|
|
|
|
Raises:
|
|
CannotHandleCompletionError: If FindCompletions cannot handle completion.
|
|
|
|
Returns:
|
|
[]: No completions.
|
|
[completions]: List, all possible sorted completions.
|
|
"""
|
|
words = _GetCmdWordQueue(cmd_line)
|
|
node = root
|
|
|
|
global_flags = node[LOOKUP_FLAGS]
|
|
|
|
completions = []
|
|
flag_mode = FLAG_BOOLEAN
|
|
|
|
env_var_prefix = GetEnvVarPrefix()
|
|
env_vars = os.environ
|
|
while words:
|
|
word = words.pop()
|
|
|
|
if word.startswith(FLAG_PREFIX):
|
|
is_flag_word = True
|
|
child_nodes = node.get(LOOKUP_FLAGS, {})
|
|
child_nodes.update(global_flags)
|
|
# Add the value part back to the queue if it exists
|
|
if _VALUE_SEP in word:
|
|
word, flag_value = word.split(_VALUE_SEP, 1)
|
|
# This predates the env var completion but is necessary for completing
|
|
# environment variables that are flag values.
|
|
words.append(flag_value)
|
|
elif word.startswith(env_var_prefix):
|
|
is_flag_word = False
|
|
child_nodes = env_vars
|
|
flag_mode = ENV_VAR
|
|
else:
|
|
is_flag_word = False
|
|
child_nodes = node.get(LOOKUP_COMMANDS, {})
|
|
|
|
# Consume word
|
|
if words:
|
|
if word in child_nodes:
|
|
if is_flag_word:
|
|
flag_mode = child_nodes[word]
|
|
else:
|
|
flag_mode = FLAG_BOOLEAN
|
|
node = child_nodes[word] # Progress to next command node
|
|
elif flag_mode == ENV_VAR:
|
|
continue
|
|
elif flag_mode != FLAG_BOOLEAN:
|
|
flag_mode = FLAG_BOOLEAN
|
|
continue # Just consume if we are expecting a flag value
|
|
elif not is_flag_word and not node.get(LOOKUP_COMMANDS):
|
|
# If we're at a leaf command node, this could be a positional arg, so
|
|
# consume it and move on.
|
|
flag_mode = FLAG_BOOLEAN
|
|
continue
|
|
else:
|
|
return [] # Non-existing command/flag, so nothing to do
|
|
|
|
# Complete word
|
|
else:
|
|
if flag_mode == FLAG_DYNAMIC:
|
|
raise CannotHandleCompletionError(
|
|
'Dynamic completions are not handled by this module')
|
|
elif flag_mode == FLAG_VALUE:
|
|
return [] # Cannot complete, so nothing to do
|
|
elif flag_mode == ENV_VAR:
|
|
completions += MatchEnvVars(word, child_nodes)
|
|
elif flag_mode != FLAG_BOOLEAN: # Must be list of choices
|
|
for value in flag_mode:
|
|
if value.startswith(word):
|
|
completions.append(value)
|
|
elif not child_nodes:
|
|
raise CannotHandleCompletionError(
|
|
'Positional completions are not handled by this module')
|
|
else: # Command/flag completion
|
|
for child, value in six.iteritems(child_nodes):
|
|
if not child.startswith(word):
|
|
continue
|
|
if is_flag_word and value != FLAG_BOOLEAN:
|
|
child += _VALUE_SEP
|
|
completions.append(child)
|
|
return sorted(completions)
|
|
|
|
|
|
def _GetInstallationRootDir():
|
|
"""Returns the SDK installation root dir."""
|
|
# Intentionally ignoring config path abstraction imports.
|
|
return os.path.sep.join(__file__.split(os.path.sep)[:-5])
|
|
|
|
|
|
def _GetCompletionCliTreeDir():
|
|
"""Returns the SDK static completion CLI tree dir."""
|
|
# Intentionally ignoring config path abstraction imports.
|
|
return os.path.join(_GetInstallationRootDir(), 'data', 'cli')
|
|
|
|
|
|
def CompletionCliTreePath(directory=None):
|
|
"""Returns the SDK static completion CLI tree path."""
|
|
# Intentionally ignoring config path abstraction imports.
|
|
return os.path.join(
|
|
directory or _GetCompletionCliTreeDir(), 'gcloud_completions.py')
|
|
|
|
|
|
def LoadCompletionCliTree():
|
|
"""Loads and returns the static completion CLI tree."""
|
|
try:
|
|
sys_path = sys.path[:]
|
|
sys.path.append(_GetCompletionCliTreeDir())
|
|
import gcloud_completions # pylint: disable=g-import-not-at-top
|
|
tree = gcloud_completions.STATIC_COMPLETION_CLI_TREE
|
|
except ImportError:
|
|
raise CannotHandleCompletionError(
|
|
'Cannot find static completion CLI tree module.')
|
|
finally:
|
|
sys.path = sys_path
|
|
return tree
|
|
|
|
|
|
def _OpenCompletionsOutputStream():
|
|
"""Returns the completions output stream."""
|
|
return os.fdopen(COMPLETIONS_OUTPUT_FD, 'wb')
|
|
|
|
|
|
def _GetCompletions():
|
|
"""Returns the static completions, None if there are none."""
|
|
root = LoadCompletionCliTree()
|
|
cmd_line = _GetCmdLineFromEnv()
|
|
return _FindCompletions(root, cmd_line)
|
|
|
|
|
|
def Complete():
|
|
"""Attempts completions and writes them to the completion stream."""
|
|
completions = _GetCompletions()
|
|
if completions:
|
|
# The bash/zsh completion scripts set IFS_ENV_VAR to one character.
|
|
ifs = encoding.GetEncodedValue(os.environ, IFS_ENV_VAR, IFS_ENV_DEFAULT)
|
|
# Write completions to stream
|
|
f = None
|
|
try:
|
|
f = _OpenCompletionsOutputStream()
|
|
# the other side also uses the console encoding
|
|
f.write(ifs.join(completions).encode())
|
|
finally:
|
|
if f:
|
|
f.close()
|