feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
from __future__ import unicode_literals
from .filesystem import PathCompleter
from .base import WordCompleter
from .system import SystemCompleter

View File

@@ -0,0 +1,61 @@
from __future__ import unicode_literals
from six import string_types
from prompt_toolkit.completion import Completer, Completion
__all__ = (
'WordCompleter',
)
class WordCompleter(Completer):
"""
Simple autocompletion on a list of words.
:param words: List of words.
:param ignore_case: If True, case-insensitive completion.
:param meta_dict: Optional dict mapping words to their meta-information.
:param WORD: When True, use WORD characters.
:param sentence: When True, don't complete by comparing the word before the
cursor, but by comparing all the text before the cursor. In this case,
the list of words is just a list of strings, where each string can
contain spaces. (Can not be used together with the WORD option.)
:param match_middle: When True, match not only the start, but also in the
middle of the word.
"""
def __init__(self, words, ignore_case=False, meta_dict=None, WORD=False,
sentence=False, match_middle=False):
assert not (WORD and sentence)
self.words = list(words)
self.ignore_case = ignore_case
self.meta_dict = meta_dict or {}
self.WORD = WORD
self.sentence = sentence
self.match_middle = match_middle
assert all(isinstance(w, string_types) for w in self.words)
def get_completions(self, document, complete_event):
# Get word/text before cursor.
if self.sentence:
word_before_cursor = document.text_before_cursor
else:
word_before_cursor = document.get_word_before_cursor(WORD=self.WORD)
if self.ignore_case:
word_before_cursor = word_before_cursor.lower()
def word_matches(word):
""" True when the word before the cursor matches. """
if self.ignore_case:
word = word.lower()
if self.match_middle:
return word_before_cursor in word
else:
return word.startswith(word_before_cursor)
for a in self.words:
if word_matches(a):
display_meta = self.meta_dict.get(a, '')
yield Completion(a, -len(word_before_cursor), display_meta=display_meta)

View File

@@ -0,0 +1,105 @@
from __future__ import unicode_literals
from prompt_toolkit.completion import Completer, Completion
import os
__all__ = (
'PathCompleter',
'ExecutableCompleter',
)
class PathCompleter(Completer):
"""
Complete for Path variables.
:param get_paths: Callable which returns a list of directories to look into
when the user enters a relative path.
:param file_filter: Callable which takes a filename and returns whether
this file should show up in the completion. ``None``
when no filtering has to be done.
:param min_input_len: Don't do autocompletion when the input string is shorter.
"""
def __init__(self, only_directories=False, get_paths=None, file_filter=None,
min_input_len=0, expanduser=False):
assert get_paths is None or callable(get_paths)
assert file_filter is None or callable(file_filter)
assert isinstance(min_input_len, int)
assert isinstance(expanduser, bool)
self.only_directories = only_directories
self.get_paths = get_paths or (lambda: ['.'])
self.file_filter = file_filter or (lambda _: True)
self.min_input_len = min_input_len
self.expanduser = expanduser
def get_completions(self, document, complete_event):
text = document.text_before_cursor
# Complete only when we have at least the minimal input length,
# otherwise, we can too many results and autocompletion will become too
# heavy.
if len(text) < self.min_input_len:
return
try:
# Do tilde expansion.
if self.expanduser:
text = os.path.expanduser(text)
# Directories where to look.
dirname = os.path.dirname(text)
if dirname:
directories = [os.path.dirname(os.path.join(p, text))
for p in self.get_paths()]
else:
directories = self.get_paths()
# Start of current file.
prefix = os.path.basename(text)
# Get all filenames.
filenames = []
for directory in directories:
# Look for matches in this directory.
if os.path.isdir(directory):
for filename in os.listdir(directory):
if filename.startswith(prefix):
filenames.append((directory, filename))
# Sort
filenames = sorted(filenames, key=lambda k: k[1])
# Yield them.
for directory, filename in filenames:
completion = filename[len(prefix):]
full_name = os.path.join(directory, filename)
if os.path.isdir(full_name):
# For directories, add a slash to the filename.
# (We don't add them to the `completion`. Users can type it
# to trigger the autocompletion themself.)
filename += '/'
elif self.only_directories:
continue
if not self.file_filter(full_name):
continue
yield Completion(completion, 0, display=filename)
except OSError:
pass
class ExecutableCompleter(PathCompleter):
"""
Complete only excutable files in the current path.
"""
def __init__(self):
PathCompleter.__init__(
self,
only_directories=False,
min_input_len=1,
get_paths=lambda: os.environ.get('PATH', '').split(os.pathsep),
file_filter=lambda name: os.access(name, os.X_OK),
expanduser=True),

View File

@@ -0,0 +1,56 @@
from __future__ import unicode_literals
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
from prompt_toolkit.contrib.regular_languages.compiler import compile
from .filesystem import PathCompleter, ExecutableCompleter
__all__ = (
'SystemCompleter',
)
class SystemCompleter(GrammarCompleter):
"""
Completer for system commands.
"""
def __init__(self):
# Compile grammar.
g = compile(
r"""
# First we have an executable.
(?P<executable>[^\s]+)
# Ignore literals in between.
(
\s+
("[^"]*" | '[^']*' | [^'"]+ )
)*
\s+
# Filename as parameters.
(
(?P<filename>[^\s]+) |
"(?P<double_quoted_filename>[^\s]+)" |
'(?P<single_quoted_filename>[^\s]+)'
)
""",
escape_funcs={
'double_quoted_filename': (lambda string: string.replace('"', '\\"')),
'single_quoted_filename': (lambda string: string.replace("'", "\\'")),
},
unescape_funcs={
'double_quoted_filename': (lambda string: string.replace('\\"', '"')), # XXX: not enterily correct.
'single_quoted_filename': (lambda string: string.replace("\\'", "'")),
})
# Create GrammarCompleter
super(SystemCompleter, self).__init__(
g,
{
'executable': ExecutableCompleter(),
'filename': PathCompleter(only_directories=False, expanduser=True),
'double_quoted_filename': PathCompleter(only_directories=False, expanduser=True),
'single_quoted_filename': PathCompleter(only_directories=False, expanduser=True),
})

View File

@@ -0,0 +1,76 @@
r"""
Tool for expressing the grammar of an input as a regular language.
==================================================================
The grammar for the input of many simple command line interfaces can be
expressed by a regular language. Examples are PDB (the Python debugger); a
simple (bash-like) shell with "pwd", "cd", "cat" and "ls" commands; arguments
that you can pass to an executable; etc. It is possible to use regular
expressions for validation and parsing of such a grammar. (More about regular
languages: http://en.wikipedia.org/wiki/Regular_language)
Example
-------
Let's take the pwd/cd/cat/ls example. We want to have a shell that accepts
these three commands. "cd" is followed by a quoted directory name and "cat" is
followed by a quoted file name. (We allow quotes inside the filename when
they're escaped with a backslash.) We could define the grammar using the
following regular expression::
grammar = \s* (
pwd |
ls |
(cd \s+ " ([^"]|\.)+ ") |
(cat \s+ " ([^"]|\.)+ ")
) \s*
What can we do with this grammar?
---------------------------------
- Syntax highlighting: We could use this for instance to give file names
different colour.
- Parse the result: .. We can extract the file names and commands by using a
regular expression with named groups.
- Input validation: .. Don't accept anything that does not match this grammar.
When combined with a parser, we can also recursively do
filename validation (and accept only existing files.)
- Autocompletion: .... Each part of the grammar can have its own autocompleter.
"cat" has to be completed using file names, while "cd"
has to be completed using directory names.
How does it work?
-----------------
As a user of this library, you have to define the grammar of the input as a
regular expression. The parts of this grammar where autocompletion, validation
or any other processing is required need to be marked using a regex named
group. Like ``(?P<varname>...)`` for instance.
When the input is processed for validation (for instance), the regex will
execute, the named group is captured, and the validator associated with this
named group will test the captured string.
There is one tricky bit:
Ofter we operate on incomplete input (this is by definition the case for
autocompletion) and we have to decide for the cursor position in which
possible state the grammar it could be and in which way variables could be
matched up to that point.
To solve this problem, the compiler takes the original regular expression and
translates it into a set of other regular expressions which each match prefixes
of strings that would match the first expression. (We translate it into
multiple expression, because we want to have each possible state the regex
could be in -- in case there are several or-clauses with each different
completers.)
TODO: some examples of:
- How to create a highlighter from this grammar.
- How to create a validator from this grammar.
- How to create an autocompleter from this grammar.
- How to create a parser from this grammar.
"""
from .compiler import compile

View File

@@ -0,0 +1,408 @@
r"""
Compiler for a regular grammar.
Example usage::
# Create and compile grammar.
p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)')
# Match input string.
m = p.match('add 23 432')
# Get variables.
m.variables().get('var1') # Returns "23"
m.variables().get('var2') # Returns "432"
Partial matches are possible::
# Create and compile grammar.
p = compile('''
# Operators with two arguments.
((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) |
# Operators with only one arguments.
((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+))
''')
# Match partial input string.
m = p.match_prefix('add 23')
# Get variables. (Notice that both operator1 and operator2 contain the
# value "add".) This is because our input is incomplete, and we don't know
# yet in which rule of the regex we we'll end up. It could also be that
# `operator1` and `operator2` have a different autocompleter and we want to
# call all possible autocompleters that would result in valid input.)
m.variables().get('var1') # Returns "23"
m.variables().get('operator1') # Returns "add"
m.variables().get('operator2') # Returns "add"
"""
from __future__ import unicode_literals
import re
from six.moves import range
from .regex_parser import Any, Sequence, Regex, Variable, Repeat, Lookahead
from .regex_parser import parse_regex, tokenize_regex
__all__ = (
'compile',
)
# Name of the named group in the regex, matching trailing input.
# (Trailing input is when the input contains characters after the end of the
# expression has been matched.)
_INVALID_TRAILING_INPUT = 'invalid_trailing'
class _CompiledGrammar(object):
"""
Compiles a grammar. This will take the parse tree of a regular expression
and compile the grammar.
:param root_node: :class~`.regex_parser.Node` instance.
:param escape_funcs: `dict` mapping variable names to escape callables.
:param unescape_funcs: `dict` mapping variable names to unescape callables.
"""
def __init__(self, root_node, escape_funcs=None, unescape_funcs=None):
self.root_node = root_node
self.escape_funcs = escape_funcs or {}
self.unescape_funcs = unescape_funcs or {}
#: Dictionary that will map the redex names to Node instances.
self._group_names_to_nodes = {} # Maps regex group names to varnames.
counter = [0]
def create_group_func(node):
name = 'n%s' % counter[0]
self._group_names_to_nodes[name] = node.varname
counter[0] += 1
return name
# Compile regex strings.
self._re_pattern = '^%s$' % self._transform(root_node, create_group_func)
self._re_prefix_patterns = list(self._transform_prefix(root_node, create_group_func))
# Compile the regex itself.
flags = re.DOTALL # Note that we don't need re.MULTILINE! (^ and $
# still represent the start and end of input text.)
self._re = re.compile(self._re_pattern, flags)
self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns]
# We compile one more set of regexes, similar to `_re_prefix`, but accept any trailing
# input. This will ensure that we can still highlight the input correctly, even when the
# input contains some additional characters at the end that don't match the grammar.)
self._re_prefix_with_trailing_input = [
re.compile(r'(?:%s)(?P<%s>.*?)$' % (t.rstrip('$'), _INVALID_TRAILING_INPUT), flags)
for t in self._re_prefix_patterns]
def escape(self, varname, value):
"""
Escape `value` to fit in the place of this variable into the grammar.
"""
f = self.escape_funcs.get(varname)
return f(value) if f else value
def unescape(self, varname, value):
"""
Unescape `value`.
"""
f = self.unescape_funcs.get(varname)
return f(value) if f else value
@classmethod
def _transform(cls, root_node, create_group_func):
"""
Turn a :class:`Node` object into a regular expression.
:param root_node: The :class:`Node` instance for which we generate the grammar.
:param create_group_func: A callable which takes a `Node` and returns the next
free name for this node.
"""
def transform(node):
# Turn `Any` into an OR.
if isinstance(node, Any):
return '(?:%s)' % '|'.join(transform(c) for c in node.children)
# Concatenate a `Sequence`
elif isinstance(node, Sequence):
return ''.join(transform(c) for c in node.children)
# For Regex and Lookahead nodes, just insert them literally.
elif isinstance(node, Regex):
return node.regex
elif isinstance(node, Lookahead):
before = ('(?!' if node.negative else '(=')
return before + transform(node.childnode) + ')'
# A `Variable` wraps the children into a named group.
elif isinstance(node, Variable):
return '(?P<%s>%s)' % (create_group_func(node), transform(node.childnode))
# `Repeat`.
elif isinstance(node, Repeat):
return '(?:%s){%i,%s}%s' % (
transform(node.childnode), node.min_repeat,
('' if node.max_repeat is None else str(node.max_repeat)),
('' if node.greedy else '?')
)
else:
raise TypeError('Got %r' % (node, ))
return transform(root_node)
@classmethod
def _transform_prefix(cls, root_node, create_group_func):
"""
Yield all the regular expressions matching a prefix of the grammar
defined by the `Node` instance.
This can yield multiple expressions, because in the case of on OR
operation in the grammar, we can have another outcome depending on
which clause would appear first. E.g. "(A|B)C" is not the same as
"(B|A)C" because the regex engine is lazy and takes the first match.
However, because we the current input is actually a prefix of the
grammar which meight not yet contain the data for "C", we need to know
both intermediate states, in order to call the appropriate
autocompletion for both cases.
:param root_node: The :class:`Node` instance for which we generate the grammar.
:param create_group_func: A callable which takes a `Node` and returns the next
free name for this node.
"""
def transform(node):
# Generate regexes for all permutations of this OR. Each node
# should be in front once.
if isinstance(node, Any):
for c in node.children:
for r in transform(c):
yield '(?:%s)?' % r
# For a sequence. We can either have a match for the sequence
# of all the children, or for an exact match of the first X
# children, followed by a partial match of the next children.
elif isinstance(node, Sequence):
for i in range(len(node.children)):
a = [cls._transform(c, create_group_func) for c in node.children[:i]]
for c in transform(node.children[i]):
yield '(?:%s)' % (''.join(a) + c)
elif isinstance(node, Regex):
yield '(?:%s)?' % node.regex
elif isinstance(node, Lookahead):
if node.negative:
yield '(?!%s)' % cls._transform(node.childnode, create_group_func)
else:
# Not sure what the correct semantics are in this case.
# (Probably it's not worth implementing this.)
raise Exception('Positive lookahead not yet supported.')
elif isinstance(node, Variable):
# (Note that we should not append a '?' here. the 'transform'
# method will already recursively do that.)
for c in transform(node.childnode):
yield '(?P<%s>%s)' % (create_group_func(node), c)
elif isinstance(node, Repeat):
# If we have a repetition of 8 times. That would mean that the
# current input could have for instance 7 times a complete
# match, followed by a partial match.
prefix = cls._transform(node.childnode, create_group_func)
for c in transform(node.childnode):
if node.max_repeat:
repeat_sign = '{,%i}' % (node.max_repeat - 1)
else:
repeat_sign = '*'
yield '(?:%s)%s%s(?:%s)?' % (
prefix,
repeat_sign,
('' if node.greedy else '?'),
c)
else:
raise TypeError('Got %r' % node)
for r in transform(root_node):
yield '^%s$' % r
def match(self, string):
"""
Match the string with the grammar.
Returns a :class:`Match` instance or `None` when the input doesn't match the grammar.
:param string: The input string.
"""
m = self._re.match(string)
if m:
return Match(string, [(self._re, m)], self._group_names_to_nodes, self.unescape_funcs)
def match_prefix(self, string):
"""
Do a partial match of the string with the grammar. The returned
:class:`Match` instance can contain multiple representations of the
match. This will never return `None`. If it doesn't match at all, the "trailing input"
part will capture all of the input.
:param string: The input string.
"""
# First try to match using `_re_prefix`. If nothing is found, use the patterns that
# also accept trailing characters.
for patterns in [self._re_prefix, self._re_prefix_with_trailing_input]:
matches = [(r, r.match(string)) for r in patterns]
matches = [(r, m) for r, m in matches if m]
if matches != []:
return Match(string, matches, self._group_names_to_nodes, self.unescape_funcs)
class Match(object):
"""
:param string: The input string.
:param re_matches: List of (compiled_re_pattern, re_match) tuples.
:param group_names_to_nodes: Dictionary mapping all the re group names to the matching Node instances.
"""
def __init__(self, string, re_matches, group_names_to_nodes, unescape_funcs):
self.string = string
self._re_matches = re_matches
self._group_names_to_nodes = group_names_to_nodes
self._unescape_funcs = unescape_funcs
def _nodes_to_regs(self):
"""
Return a list of (varname, reg) tuples.
"""
def get_tuples():
for r, re_match in self._re_matches:
for group_name, group_index in r.groupindex.items():
if group_name != _INVALID_TRAILING_INPUT:
reg = re_match.regs[group_index]
node = self._group_names_to_nodes[group_name]
yield (node, reg)
return list(get_tuples())
def _nodes_to_values(self):
"""
Returns list of list of (Node, string_value) tuples.
"""
def is_none(slice):
return slice[0] == -1 and slice[1] == -1
def get(slice):
return self.string[slice[0]:slice[1]]
return [(varname, get(slice), slice) for varname, slice in self._nodes_to_regs() if not is_none(slice)]
def _unescape(self, varname, value):
unwrapper = self._unescape_funcs.get(varname)
return unwrapper(value) if unwrapper else value
def variables(self):
"""
Returns :class:`Variables` instance.
"""
return Variables([(k, self._unescape(k, v), sl) for k, v, sl in self._nodes_to_values()])
def trailing_input(self):
"""
Get the `MatchVariable` instance, representing trailing input, if there is any.
"Trailing input" is input at the end that does not match the grammar anymore, but
when this is removed from the end of the input, the input would be a valid string.
"""
slices = []
# Find all regex group for the name _INVALID_TRAILING_INPUT.
for r, re_match in self._re_matches:
for group_name, group_index in r.groupindex.items():
if group_name == _INVALID_TRAILING_INPUT:
slices.append(re_match.regs[group_index])
# Take the smallest part. (Smaller trailing text means that a larger input has
# been matched, so that is better.)
if slices:
slice = [max(i[0] for i in slices), max(i[1] for i in slices)]
value = self.string[slice[0]:slice[1]]
return MatchVariable('<trailing_input>', value, slice)
def end_nodes(self):
"""
Yields `MatchVariable` instances for all the nodes having their end
position at the end of the input string.
"""
for varname, reg in self._nodes_to_regs():
# If this part goes until the end of the input string.
if reg[1] == len(self.string):
value = self._unescape(varname, self.string[reg[0]: reg[1]])
yield MatchVariable(varname, value, (reg[0], reg[1]))
class Variables(object):
def __init__(self, tuples):
#: List of (varname, value, slice) tuples.
self._tuples = tuples
def __repr__(self):
return '%s(%s)' % (
self.__class__.__name__, ', '.join('%s=%r' % (k, v) for k, v, _ in self._tuples))
def get(self, key, default=None):
items = self.getall(key)
return items[0] if items else default
def getall(self, key):
return [v for k, v, _ in self._tuples if k == key]
def __getitem__(self, key):
return self.get(key)
def __iter__(self):
"""
Yield `MatchVariable` instances.
"""
for varname, value, slice in self._tuples:
yield MatchVariable(varname, value, slice)
class MatchVariable(object):
"""
Represents a match of a variable in the grammar.
:param varname: (string) Name of the variable.
:param value: (string) Value of this variable.
:param slice: (start, stop) tuple, indicating the position of this variable
in the input string.
"""
def __init__(self, varname, value, slice):
self.varname = varname
self.value = value
self.slice = slice
self.start = self.slice[0]
self.stop = self.slice[1]
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.varname, self.value)
def compile(expression, escape_funcs=None, unescape_funcs=None):
"""
Compile grammar (given as regex string), returning a `CompiledGrammar`
instance.
"""
return _compile_from_parse_tree(
parse_regex(tokenize_regex(expression)),
escape_funcs=escape_funcs,
unescape_funcs=unescape_funcs)
def _compile_from_parse_tree(root_node, *a, **kw):
"""
Compile grammar (given as parse tree), returning a `CompiledGrammar`
instance.
"""
return _CompiledGrammar(root_node, *a, **kw)

View File

@@ -0,0 +1,84 @@
"""
Completer for a regular grammar.
"""
from __future__ import unicode_literals
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document
from .compiler import _CompiledGrammar
__all__ = (
'GrammarCompleter',
)
class GrammarCompleter(Completer):
"""
Completer which can be used for autocompletion according to variables in
the grammar. Each variable can have a different autocompleter.
:param compiled_grammar: `GrammarCompleter` instance.
:param completers: `dict` mapping variable names of the grammar to the
`Completer` instances to be used for each variable.
"""
def __init__(self, compiled_grammar, completers):
assert isinstance(compiled_grammar, _CompiledGrammar)
assert isinstance(completers, dict)
self.compiled_grammar = compiled_grammar
self.completers = completers
def get_completions(self, document, complete_event):
m = self.compiled_grammar.match_prefix(document.text_before_cursor)
if m:
completions = self._remove_duplicates(
self._get_completions_for_match(m, complete_event))
for c in completions:
yield c
def _get_completions_for_match(self, match, complete_event):
"""
Yield all the possible completions for this input string.
(The completer assumes that the cursor position was at the end of the
input string.)
"""
for match_variable in match.end_nodes():
varname = match_variable.varname
start = match_variable.start
completer = self.completers.get(varname)
if completer:
text = match_variable.value
# Unwrap text.
unwrapped_text = self.compiled_grammar.unescape(varname, text)
# Create a document, for the completions API (text/cursor_position)
document = Document(unwrapped_text, len(unwrapped_text))
# Call completer
for completion in completer.get_completions(document, complete_event):
new_text = unwrapped_text[:len(text) + completion.start_position] + completion.text
# Wrap again.
yield Completion(
text=self.compiled_grammar.escape(varname, new_text),
start_position=start - len(match.string),
display=completion.display,
display_meta=completion.display_meta)
def _remove_duplicates(self, items):
"""
Remove duplicates, while keeping the order.
(Sometimes we have duplicates, because the there several matches of the
same grammar, each yielding similar completions.)
"""
result = []
for i in items:
if i not in result:
result.append(i)
return result

View File

@@ -0,0 +1,90 @@
"""
`GrammarLexer` is compatible with Pygments lexers and can be used to highlight
the input using a regular grammar with token annotations.
"""
from __future__ import unicode_literals
from prompt_toolkit.document import Document
from prompt_toolkit.layout.lexers import Lexer
from prompt_toolkit.layout.utils import split_lines
from prompt_toolkit.token import Token
from .compiler import _CompiledGrammar
from six.moves import range
__all__ = (
'GrammarLexer',
)
class GrammarLexer(Lexer):
"""
Lexer which can be used for highlighting of tokens according to variables in the grammar.
(It does not actual lexing of the string, but it exposes an API, compatible
with the Pygments lexer class.)
:param compiled_grammar: Grammar as returned by the `compile()` function.
:param lexers: Dictionary mapping variable names of the regular grammar to
the lexers that should be used for this part. (This can
call other lexers recursively.) If you wish a part of the
grammar to just get one token, use a
`prompt_toolkit.layout.lexers.SimpleLexer`.
"""
def __init__(self, compiled_grammar, default_token=None, lexers=None):
assert isinstance(compiled_grammar, _CompiledGrammar)
assert default_token is None or isinstance(default_token, tuple)
assert lexers is None or all(isinstance(v, Lexer) for k, v in lexers.items())
assert lexers is None or isinstance(lexers, dict)
self.compiled_grammar = compiled_grammar
self.default_token = default_token or Token
self.lexers = lexers or {}
def _get_tokens(self, cli, text):
m = self.compiled_grammar.match_prefix(text)
if m:
characters = [[self.default_token, c] for c in text]
for v in m.variables():
# If we have a `Lexer` instance for this part of the input.
# Tokenize recursively and apply tokens.
lexer = self.lexers.get(v.varname)
if lexer:
document = Document(text[v.start:v.stop])
lexer_tokens_for_line = lexer.lex_document(cli, document)
lexer_tokens = []
for i in range(len(document.lines)):
lexer_tokens.extend(lexer_tokens_for_line(i))
lexer_tokens.append((Token, '\n'))
if lexer_tokens:
lexer_tokens.pop()
i = v.start
for t, s in lexer_tokens:
for c in s:
if characters[i][0] == self.default_token:
characters[i][0] = t
i += 1
# Highlight trailing input.
trailing_input = m.trailing_input()
if trailing_input:
for i in range(trailing_input.start, trailing_input.stop):
characters[i][0] = Token.TrailingInput
return characters
else:
return [(Token, text)]
def lex_document(self, cli, document):
lines = list(split_lines(self._get_tokens(cli, document.text)))
def get_line(lineno):
try:
return lines[lineno]
except IndexError:
return []
return get_line

View File

@@ -0,0 +1,262 @@
"""
Parser for parsing a regular expression.
Take a string representing a regular expression and return the root node of its
parse tree.
usage::
root_node = parse_regex('(hello|world)')
Remarks:
- The regex parser processes multiline, it ignores all whitespace and supports
multiple named groups with the same name and #-style comments.
Limitations:
- Lookahead is not supported.
"""
from __future__ import unicode_literals
import re
__all__ = (
'Repeat',
'Variable',
'Regex',
'Lookahead',
'tokenize_regex',
'parse_regex',
)
class Node(object):
"""
Base class for all the grammar nodes.
(You don't initialize this one.)
"""
def __add__(self, other_node):
return Sequence([self, other_node])
def __or__(self, other_node):
return Any([self, other_node])
class Any(Node):
"""
Union operation (OR operation) between several grammars. You don't
initialize this yourself, but it's a result of a "Grammar1 | Grammar2"
operation.
"""
def __init__(self, children):
self.children = children
def __or__(self, other_node):
return Any(self.children + [other_node])
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.children)
class Sequence(Node):
"""
Concatenation operation of several grammars. You don't initialize this
yourself, but it's a result of a "Grammar1 + Grammar2" operation.
"""
def __init__(self, children):
self.children = children
def __add__(self, other_node):
return Sequence(self.children + [other_node])
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.children)
class Regex(Node):
"""
Regular expression.
"""
def __init__(self, regex):
re.compile(regex) # Validate
self.regex = regex
def __repr__(self):
return '%s(/%s/)' % (self.__class__.__name__, self.regex)
class Lookahead(Node):
"""
Lookahead expression.
"""
def __init__(self, childnode, negative=False):
self.childnode = childnode
self.negative = negative
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.childnode)
class Variable(Node):
"""
Mark a variable in the regular grammar. This will be translated into a
named group. Each variable can have his own completer, validator, etc..
:param childnode: The grammar which is wrapped inside this variable.
:param varname: String.
"""
def __init__(self, childnode, varname=None):
self.childnode = childnode
self.varname = varname
def __repr__(self):
return '%s(childnode=%r, varname=%r)' % (
self.__class__.__name__, self.childnode, self.varname)
class Repeat(Node):
def __init__(self, childnode, min_repeat=0, max_repeat=None, greedy=True):
self.childnode = childnode
self.min_repeat = min_repeat
self.max_repeat = max_repeat
self.greedy = greedy
def __repr__(self):
return '%s(childnode=%r)' % (self.__class__.__name__, self.childnode)
def tokenize_regex(input):
"""
Takes a string, representing a regular expression as input, and tokenizes
it.
:param input: string, representing a regular expression.
:returns: List of tokens.
"""
# Regular expression for tokenizing other regular expressions.
p = re.compile(r'''^(
\(\?P\<[a-zA-Z0-9_-]+\> | # Start of named group.
\(\?#[^)]*\) | # Comment
\(\?= | # Start of lookahead assertion
\(\?! | # Start of negative lookahead assertion
\(\?<= | # If preceded by.
\(\?< | # If not preceded by.
\(?: | # Start of group. (non capturing.)
\( | # Start of group.
\(?[iLmsux] | # Flags.
\(?P=[a-zA-Z]+\) | # Back reference to named group
\) | # End of group.
\{[^{}]*\} | # Repetition
\*\? | \+\? | \?\?\ | # Non greedy repetition.
\* | \+ | \? | # Repetition
\#.*\n | # Comment
\\. |
# Character group.
\[
( [^\]\\] | \\.)*
\] |
[^(){}] |
.
)''', re.VERBOSE)
tokens = []
while input:
m = p.match(input)
if m:
token, input = input[:m.end()], input[m.end():]
if not token.isspace():
tokens.append(token)
else:
raise Exception('Could not tokenize input regex.')
return tokens
def parse_regex(regex_tokens):
"""
Takes a list of tokens from the tokenizer, and returns a parse tree.
"""
# We add a closing brace because that represents the final pop of the stack.
tokens = [')'] + regex_tokens[::-1]
def wrap(lst):
""" Turn list into sequence when it contains several items. """
if len(lst) == 1:
return lst[0]
else:
return Sequence(lst)
def _parse():
or_list = []
result = []
def wrapped_result():
if or_list == []:
return wrap(result)
else:
or_list.append(result)
return Any([wrap(i) for i in or_list])
while tokens:
t = tokens.pop()
if t.startswith('(?P<'):
variable = Variable(_parse(), varname=t[4:-1])
result.append(variable)
elif t in ('*', '*?'):
greedy = (t == '*')
result[-1] = Repeat(result[-1], greedy=greedy)
elif t in ('+', '+?'):
greedy = (t == '+')
result[-1] = Repeat(result[-1], min_repeat=1, greedy=greedy)
elif t in ('?', '??'):
if result == []:
raise Exception('Nothing to repeat.' + repr(tokens))
else:
greedy = (t == '?')
result[-1] = Repeat(result[-1], min_repeat=0, max_repeat=1, greedy=greedy)
elif t == '|':
or_list.append(result)
result = []
elif t in ('(', '(?:'):
result.append(_parse())
elif t == '(?!':
result.append(Lookahead(_parse(), negative=True))
elif t == '(?=':
result.append(Lookahead(_parse(), negative=False))
elif t == ')':
return wrapped_result()
elif t.startswith('#'):
pass
elif t.startswith('{'):
# TODO: implement!
raise Exception('{}-style repitition not yet supported' % t)
elif t.startswith('(?'):
raise Exception('%r not supported' % t)
elif t.isspace():
pass
else:
result.append(Regex(t))
raise Exception("Expecting ')' token")
result = _parse()
if len(tokens) != 0:
raise Exception("Unmatched parantheses.")
else:
return result

View File

@@ -0,0 +1,57 @@
"""
Validator for a regular langage.
"""
from __future__ import unicode_literals
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.document import Document
from .compiler import _CompiledGrammar
__all__ = (
'GrammarValidator',
)
class GrammarValidator(Validator):
"""
Validator which can be used for validation according to variables in
the grammar. Each variable can have its own validator.
:param compiled_grammar: `GrammarCompleter` instance.
:param validators: `dict` mapping variable names of the grammar to the
`Validator` instances to be used for each variable.
"""
def __init__(self, compiled_grammar, validators):
assert isinstance(compiled_grammar, _CompiledGrammar)
assert isinstance(validators, dict)
self.compiled_grammar = compiled_grammar
self.validators = validators
def validate(self, document):
# Parse input document.
# We use `match`, not `match_prefix`, because for validation, we want
# the actual, unambiguous interpretation of the input.
m = self.compiled_grammar.match(document.text)
if m:
for v in m.variables():
validator = self.validators.get(v.varname)
if validator:
# Unescape text.
unwrapped_text = self.compiled_grammar.unescape(v.varname, v.value)
# Create a document, for the completions API (text/cursor_position)
inner_document = Document(unwrapped_text, len(unwrapped_text))
try:
validator.validate(inner_document)
except ValidationError as e:
raise ValidationError(
cursor_position=v.start + e.cursor_position,
message=e.message)
else:
raise ValidationError(cursor_position=len(document.text),
message='Invalid command')

View File

@@ -0,0 +1,2 @@
from .server import *
from .application import *

View File

@@ -0,0 +1,32 @@
"""
Interface for Telnet applications.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
__all__ = (
'TelnetApplication',
)
class TelnetApplication(with_metaclass(ABCMeta, object)):
"""
The interface which has to be implemented for any telnet application.
An instance of this class has to be passed to `TelnetServer`.
"""
@abstractmethod
def client_connected(self, telnet_connection):
"""
Called when a new client was connected.
Probably you want to call `telnet_connection.set_cli` here to set a
the CommandLineInterface instance to be used.
Hint: Use the following shortcut: `prompt_toolkit.shortcuts.create_cli`
"""
@abstractmethod
def client_leaving(self, telnet_connection):
"""
Called when a client quits.
"""

View File

@@ -0,0 +1,11 @@
"""
Python logger for the telnet server.
"""
from __future__ import unicode_literals
import logging
logger = logging.getLogger(__package__)
__all__ = (
'logger',
)

View File

@@ -0,0 +1,181 @@
"""
Parser for the Telnet protocol. (Not a complete implementation of the telnet
specification, but sufficient for a command line interface.)
Inspired by `Twisted.conch.telnet`.
"""
from __future__ import unicode_literals
import struct
from six import int2byte, binary_type, iterbytes
from .log import logger
__all__ = (
'TelnetProtocolParser',
)
# Telnet constants.
NOP = int2byte(0)
SGA = int2byte(3)
IAC = int2byte(255)
DO = int2byte(253)
DONT = int2byte(254)
LINEMODE = int2byte(34)
SB = int2byte(250)
WILL = int2byte(251)
WONT = int2byte(252)
MODE = int2byte(1)
SE = int2byte(240)
ECHO = int2byte(1)
NAWS = int2byte(31)
LINEMODE = int2byte(34)
SUPPRESS_GO_AHEAD = int2byte(3)
DM = int2byte(242)
BRK = int2byte(243)
IP = int2byte(244)
AO = int2byte(245)
AYT = int2byte(246)
EC = int2byte(247)
EL = int2byte(248)
GA = int2byte(249)
class TelnetProtocolParser(object):
"""
Parser for the Telnet protocol.
Usage::
def data_received(data):
print(data)
def size_received(rows, columns):
print(rows, columns)
p = TelnetProtocolParser(data_received, size_received)
p.feed(binary_data)
"""
def __init__(self, data_received_callback, size_received_callback):
self.data_received_callback = data_received_callback
self.size_received_callback = size_received_callback
self._parser = self._parse_coroutine()
self._parser.send(None)
def received_data(self, data):
self.data_received_callback(data)
def do_received(self, data):
""" Received telnet DO command. """
logger.info('DO %r', data)
def dont_received(self, data):
""" Received telnet DONT command. """
logger.info('DONT %r', data)
def will_received(self, data):
""" Received telnet WILL command. """
logger.info('WILL %r', data)
def wont_received(self, data):
""" Received telnet WONT command. """
logger.info('WONT %r', data)
def command_received(self, command, data):
if command == DO:
self.do_received(data)
elif command == DONT:
self.dont_received(data)
elif command == WILL:
self.will_received(data)
elif command == WONT:
self.wont_received(data)
else:
logger.info('command received %r %r', command, data)
def naws(self, data):
"""
Received NAWS. (Window dimensions.)
"""
if len(data) == 4:
# NOTE: the first parameter of struct.unpack should be
# a 'str' object. Both on Py2/py3. This crashes on OSX
# otherwise.
columns, rows = struct.unpack(str('!HH'), data)
self.size_received_callback(rows, columns)
else:
logger.warning('Wrong number of NAWS bytes')
def negotiate(self, data):
"""
Got negotiate data.
"""
command, payload = data[0:1], data[1:]
assert isinstance(command, bytes)
if command == NAWS:
self.naws(payload)
else:
logger.info('Negotiate (%r got bytes)', len(data))
def _parse_coroutine(self):
"""
Parser state machine.
Every 'yield' expression returns the next byte.
"""
while True:
d = yield
if d == int2byte(0):
pass # NOP
# Go to state escaped.
elif d == IAC:
d2 = yield
if d2 == IAC:
self.received_data(d2)
# Handle simple commands.
elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA):
self.command_received(d2, None)
# Handle IAC-[DO/DONT/WILL/WONT] commands.
elif d2 in (DO, DONT, WILL, WONT):
d3 = yield
self.command_received(d2, d3)
# Subnegotiation
elif d2 == SB:
# Consume everything until next IAC-SE
data = []
while True:
d3 = yield
if d3 == IAC:
d4 = yield
if d4 == SE:
break
else:
data.append(d4)
else:
data.append(d3)
self.negotiate(b''.join(data))
else:
self.received_data(d)
def feed(self, data):
"""
Feed data to the parser.
"""
assert isinstance(data, binary_type)
for b in iterbytes(data):
self._parser.send(int2byte(b))

View File

@@ -0,0 +1,407 @@
"""
Telnet server.
Example usage::
class MyTelnetApplication(TelnetApplication):
def client_connected(self, telnet_connection):
# Set CLI with simple prompt.
telnet_connection.set_application(
telnet_connection.create_prompt_application(...))
def handle_command(self, telnet_connection, document):
# When the client enters a command, just reply.
telnet_connection.send('You said: %r\n\n' % document.text)
...
a = MyTelnetApplication()
TelnetServer(application=a, host='127.0.0.1', port=23).run()
"""
from __future__ import unicode_literals
import socket
import select
import threading
import os
import fcntl
from six import int2byte, text_type, binary_type
from codecs import getincrementaldecoder
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.eventloop.base import EventLoop
from prompt_toolkit.interface import CommandLineInterface, Application
from prompt_toolkit.layout.screen import Size
from prompt_toolkit.shortcuts import create_prompt_application
from prompt_toolkit.terminal.vt100_input import InputStream
from prompt_toolkit.terminal.vt100_output import Vt100_Output
from .log import logger
from .protocol import IAC, DO, LINEMODE, SB, MODE, SE, WILL, ECHO, NAWS, SUPPRESS_GO_AHEAD
from .protocol import TelnetProtocolParser
from .application import TelnetApplication
__all__ = (
'TelnetServer',
)
def _initialize_telnet(connection):
logger.info('Initializing telnet connection')
# Iac Do Linemode
connection.send(IAC + DO + LINEMODE)
# Suppress Go Ahead. (This seems important for Putty to do correct echoing.)
# This will allow bi-directional operation.
connection.send(IAC + WILL + SUPPRESS_GO_AHEAD)
# Iac sb
connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE)
# IAC Will Echo
connection.send(IAC + WILL + ECHO)
# Negotiate window size
connection.send(IAC + DO + NAWS)
class _ConnectionStdout(object):
"""
Wrapper around socket which provides `write` and `flush` methods for the
Vt100_Output output.
"""
def __init__(self, connection, encoding):
self._encoding = encoding
self._connection = connection
self._buffer = []
def write(self, data):
assert isinstance(data, text_type)
self._buffer.append(data.encode(self._encoding))
self.flush()
def flush(self):
try:
self._connection.send(b''.join(self._buffer))
except socket.error as e:
logger.error("Couldn't send data over socket: %s" % e)
self._buffer = []
class TelnetConnection(object):
"""
Class that represents one Telnet connection.
"""
def __init__(self, conn, addr, application, server, encoding):
assert isinstance(addr, tuple) # (addr, port) tuple
assert isinstance(application, TelnetApplication)
assert isinstance(server, TelnetServer)
assert isinstance(encoding, text_type) # e.g. 'utf-8'
self.conn = conn
self.addr = addr
self.application = application
self.closed = False
self.handling_command = True
self.server = server
self.encoding = encoding
self.callback = None # Function that handles the CLI result.
# Create "Output" object.
self.size = Size(rows=40, columns=79)
# Initialize.
_initialize_telnet(conn)
# Create output.
def get_size():
return self.size
self.stdout = _ConnectionStdout(conn, encoding=encoding)
self.vt100_output = Vt100_Output(self.stdout, get_size, write_binary=False)
# Create an eventloop (adaptor) for the CommandLineInterface.
self.eventloop = _TelnetEventLoopInterface(server)
# Set default CommandLineInterface.
self.set_application(create_prompt_application())
# Call client_connected
application.client_connected(self)
# Draw for the first time.
self.handling_command = False
self.cli._redraw()
def set_application(self, app, callback=None):
"""
Set ``CommandLineInterface`` instance for this connection.
(This can be replaced any time.)
:param cli: CommandLineInterface instance.
:param callback: Callable that takes the result of the CLI.
"""
assert isinstance(app, Application)
assert callback is None or callable(callback)
self.cli = CommandLineInterface(
application=app,
eventloop=self.eventloop,
output=self.vt100_output)
self.callback = callback
# Create a parser, and parser callbacks.
cb = self.cli.create_eventloop_callbacks()
inputstream = InputStream(cb.feed_key)
# Input decoder for stdin. (Required when working with multibyte
# characters, like chinese input.)
stdin_decoder_cls = getincrementaldecoder(self.encoding)
stdin_decoder = [stdin_decoder_cls()] # nonlocal
# Tell the CLI that it's running. We don't start it through the run()
# call, but will still want _redraw() to work.
self.cli._is_running = True
def data_received(data):
""" TelnetProtocolParser 'data_received' callback """
assert isinstance(data, binary_type)
try:
result = stdin_decoder[0].decode(data)
inputstream.feed(result)
except UnicodeDecodeError:
stdin_decoder[0] = stdin_decoder_cls()
return ''
def size_received(rows, columns):
""" TelnetProtocolParser 'size_received' callback """
self.size = Size(rows=rows, columns=columns)
cb.terminal_size_changed()
self.parser = TelnetProtocolParser(data_received, size_received)
def feed(self, data):
"""
Handler for incoming data. (Called by TelnetServer.)
"""
assert isinstance(data, binary_type)
self.parser.feed(data)
# Render again.
self.cli._redraw()
# When a return value has been set (enter was pressed), handle command.
if self.cli.is_returning:
try:
return_value = self.cli.return_value()
except (EOFError, KeyboardInterrupt) as e:
# Control-D or Control-C was pressed.
logger.info('%s, closing connection.', type(e).__name__)
self.close()
return
# Handle CLI command
self._handle_command(return_value)
def _handle_command(self, command):
"""
Handle command. This will run in a separate thread, in order not
to block the event loop.
"""
logger.info('Handle command %r', command)
def in_executor():
self.handling_command = True
try:
if self.callback is not None:
self.callback(self, command)
finally:
self.server.call_from_executor(done)
def done():
self.handling_command = False
# Reset state and draw again. (If the connection is still open --
# the application could have called TelnetConnection.close()
if not self.closed:
self.cli.reset()
self.cli.buffers[DEFAULT_BUFFER].reset()
self.cli.renderer.request_absolute_cursor_position()
self.vt100_output.flush()
self.cli._redraw()
self.server.run_in_executor(in_executor)
def erase_screen(self):
"""
Erase output screen.
"""
self.vt100_output.erase_screen()
self.vt100_output.cursor_goto(0, 0)
self.vt100_output.flush()
def send(self, data):
"""
Send text to the client.
"""
assert isinstance(data, text_type)
# When data is send back to the client, we should replace the line
# endings. (We didn't allocate a real pseudo terminal, and the telnet
# connection is raw, so we are responsible for inserting \r.)
self.stdout.write(data.replace('\n', '\r\n'))
self.stdout.flush()
def close(self):
"""
Close the connection.
"""
self.application.client_leaving(self)
self.conn.close()
self.closed = True
class _TelnetEventLoopInterface(EventLoop):
"""
Eventloop object to be assigned to `CommandLineInterface`.
"""
def __init__(self, server):
self._server = server
def close(self):
" Ignore. "
def stop(self):
" Ignore. "
def run_in_executor(self, callback):
self._server.run_in_executor(callback)
def call_from_executor(self, callback, _max_postpone_until=None):
self._server.call_from_executor(callback)
def add_reader(self, fd, callback):
raise NotImplementedError
def remove_reader(self, fd):
raise NotImplementedError
class TelnetServer(object):
"""
Telnet server implementation.
"""
def __init__(self, host='127.0.0.1', port=23, application=None, encoding='utf-8'):
assert isinstance(host, text_type)
assert isinstance(port, int)
assert isinstance(application, TelnetApplication)
assert isinstance(encoding, text_type)
self.host = host
self.port = port
self.application = application
self.encoding = encoding
self.connections = set()
self._calls_from_executor = []
# Create a pipe for inter thread communication.
self._schedule_pipe = os.pipe()
fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK)
@classmethod
def create_socket(cls, host, port):
# Create and bind socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(4)
return s
def run_in_executor(self, callback):
threading.Thread(target=callback).start()
def call_from_executor(self, callback):
self._calls_from_executor.append(callback)
if self._schedule_pipe:
os.write(self._schedule_pipe[1], b'x')
def _process_callbacks(self):
"""
Process callbacks from `call_from_executor` in eventloop.
"""
# Flush all the pipe content.
os.read(self._schedule_pipe[0], 1024)
# Process calls from executor.
calls_from_executor, self._calls_from_executor = self._calls_from_executor, []
for c in calls_from_executor:
c()
def run(self):
"""
Run the eventloop for the telnet server.
"""
listen_socket = self.create_socket(self.host, self.port)
logger.info('Listening for telnet connections on %s port %r', self.host, self.port)
try:
while True:
# Removed closed connections.
self.connections = set([c for c in self.connections if not c.closed])
# Ignore connections handling commands.
connections = set([c for c in self.connections if not c.handling_command])
# Wait for next event.
read_list = (
[listen_socket, self._schedule_pipe[0]] +
[c.conn for c in connections])
read, _, _ = select.select(read_list, [], [])
for s in read:
# When the socket itself is ready, accept a new connection.
if s == listen_socket:
self._accept(listen_socket)
# If we receive something on our "call_from_executor" pipe, process
# these callbacks in a thread safe way.
elif s == self._schedule_pipe[0]:
self._process_callbacks()
# Handle incoming data on socket.
else:
self._handle_incoming_data(s)
finally:
listen_socket.close()
def _accept(self, listen_socket):
"""
Accept new incoming connection.
"""
conn, addr = listen_socket.accept()
connection = TelnetConnection(conn, addr, self.application, self, encoding=self.encoding)
self.connections.add(connection)
logger.info('New connection %r %r', *addr)
def _handle_incoming_data(self, conn):
"""
Handle incoming data on socket.
"""
connection = [c for c in self.connections if c.conn == conn][0]
data = conn.recv(1024)
if data:
connection.feed(data)
else:
self.connections.remove(connection)

View File

@@ -0,0 +1,34 @@
from __future__ import unicode_literals
from prompt_toolkit.validation import Validator, ValidationError
from six import string_types
class SentenceValidator(Validator):
"""
Validate input only when it appears in this list of sentences.
:param sentences: List of sentences.
:param ignore_case: If True, case-insensitive comparisons.
"""
def __init__(self, sentences, ignore_case=False, error_message='Invalid input', move_cursor_to_end=False):
assert all(isinstance(s, string_types) for s in sentences)
assert isinstance(ignore_case, bool)
assert isinstance(error_message, string_types)
self.sentences = list(sentences)
self.ignore_case = ignore_case
self.error_message = error_message
self.move_cursor_to_end = move_cursor_to_end
if ignore_case:
self.sentences = set([s.lower() for s in self.sentences])
def validate(self, document):
if document.text not in self.sentences:
if self.move_cursor_to_end:
index = len(document.text)
else:
index = 0
raise ValidationError(cursor_position=index,
message=self.error_message)