Skip to content

Commit 9149483

Browse files
committed
Merge pull request awslabs#81 from awslabs/lexer
Implements awslabs#27: Add lexer/syntax highlighting.
2 parents 5ae9053 + fd22e54 commit 9149483

File tree

5 files changed

+184
-2
lines changed

5 files changed

+184
-2
lines changed

awsshell/app.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,12 @@ def stop_input_and_refresh_cli(self):
239239
raise InputInterrupt
240240

241241
def create_layout(self, display_completions_in_columns, toolbar):
242+
from awsshell.lexer import ShellLexer
243+
lexer = ShellLexer
244+
if self.config_section['theme'] == 'none':
245+
lexer = None
242246
return create_default_layout(
243-
self, u'aws> ', reserve_space_for_menu=True,
247+
self, u'aws> ', lexer=lexer, reserve_space_for_menu=True,
244248
display_completions_in_columns=display_completions_in_columns,
245249
get_bottom_toolbar_tokens=toolbar.handler)
246250

awsshell/index/completion.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,31 @@
99
1010
"""
1111
import os
12+
import json
1213

1314
from awsshell.utils import FSLayer, FileReadError, build_config_file_path
15+
from awsshell import utils
1416

1517

1618
class IndexLoadError(Exception):
1719
"""Raised when an index could not be loaded."""
1820

1921

2022
class CompletionIndex(object):
21-
"""Handles working with the local commmand completion index."""
23+
"""Handles working with the local commmand completion index.
24+
25+
:type commands: list
26+
:param commands: ec2, s3, elb...
27+
28+
:type subcommands: list
29+
:param subcommands: start-instances, stop-instances, terminate-instances...
30+
31+
:type global_opts: list
32+
:param global_opts: --profile, --region, --output...
33+
34+
:type args_opts: set, to filter out duplicates
35+
:param args_opts: ec2 start-instances: --instance-ids, --dry-run...
36+
"""
2237

2338
# The completion index can read/write to a cache dir
2439
# so that it doesn't have to recompute the completion cache
@@ -30,13 +45,18 @@ def __init__(self, cache_dir=DEFAULT_CACHE_DIR, fslayer=None):
3045
if fslayer is None:
3146
fslayer = FSLayer()
3247
self._fslayer = fslayer
48+
self.commands = []
49+
self.subcommands = []
50+
self.global_opts = []
51+
self.args_opts = set()
3352

3453
def load_index(self, version_string):
3554
"""Load the completion index for a given CLI version.
3655
3756
:type version_string: str
3857
:param version_string: The AWS CLI version, e.g "1.9.2".
3958
59+
:raises: :class:`IndexLoadError <exceptions.IndexLoadError>`
4060
"""
4161
filename = self._filename_for_version(version_string)
4262
try:
@@ -48,3 +68,35 @@ def load_index(self, version_string):
4868
def _filename_for_version(self, version_string):
4969
return os.path.join(
5070
self._cache_dir, 'completions-%s.json' % version_string)
71+
72+
def load_completions(self):
73+
"""Loads completions from the completion index.
74+
75+
Updates the following attributes:
76+
* commands
77+
* subcommands
78+
* global_opts
79+
* args_opts
80+
"""
81+
try:
82+
index_str = self.load_index(utils.AWSCLI_VERSION)
83+
except IndexLoadError:
84+
return
85+
index_str = self.load_index(utils.AWSCLI_VERSION)
86+
index_data = json.loads(index_str)
87+
index_root = index_data['aws']
88+
# ec2, s3, elb...
89+
self.commands = index_root['commands']
90+
# --profile, --region, --output...
91+
self.global_opts = index_root['arguments']
92+
for command in self.commands:
93+
# ec2: start-instances, stop-instances, terminate-instances...
94+
subcommands_current = index_root['children'] \
95+
.get(command)['commands']
96+
self.subcommands.extend(subcommands_current)
97+
for subcommand_current in subcommands_current:
98+
# start-instances: --instance-ids, --dry-run...
99+
args_opts_current = index_root['children'] \
100+
.get(command)['children'] \
101+
.get(subcommand_current)['arguments']
102+
self.args_opts.update(args_opts_current)

awsshell/lexer.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
import ast
14+
import os
15+
16+
from pygments.lexer import RegexLexer
17+
from pygments.lexer import words
18+
from pygments.token import Keyword, Literal, Name, Operator, Text
19+
20+
from awsshell.index.completion import CompletionIndex
21+
22+
23+
class ShellLexer(RegexLexer):
24+
"""Provides highlighting for commands, subcommands, arguments, and options.
25+
26+
:type completion_index: :class:`CompletionIndex`
27+
:param completion_index: Completion index used to determine commands,
28+
subcommands, arguments, and options for highlighting.
29+
30+
:type tokens: dict
31+
:param tokens: A dict of (`pygments.lexer`, `pygments.token`) used for
32+
pygments highlighting.
33+
"""
34+
completion_index = CompletionIndex()
35+
completion_index.load_completions()
36+
tokens = {
37+
'root': [
38+
# ec2, s3, elb...
39+
(words(
40+
tuple(completion_index.commands),
41+
prefix=r'\b',
42+
suffix=r'\b'),
43+
Literal.String),
44+
# describe-instances
45+
(words(
46+
tuple(completion_index.subcommands),
47+
prefix=r'\b',
48+
suffix=r'\b'),
49+
Name.Class),
50+
# --instance-ids
51+
(words(
52+
tuple(list(completion_index.args_opts)),
53+
prefix=r'',
54+
suffix=r'\b'),
55+
Keyword.Declaration),
56+
# --profile
57+
(words(
58+
tuple(completion_index.global_opts),
59+
prefix=r'',
60+
suffix=r'\b'),
61+
Operator.Word),
62+
# Everything else
63+
(r'.*\n', Text),
64+
]
65+
}

awsshell/ui.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from prompt_toolkit.layout.toolbars import ValidationToolbar, \
1717
SystemToolbar, ArgToolbar, SearchToolbar
1818
from prompt_toolkit.layout.utils import explode_tokens
19+
from prompt_toolkit.layout.lexers import PygmentsLexer
1920
from pygments.token import Token
21+
from pygments.lexer import Lexer
2022

2123
from awsshell.compat import text_type
2224

@@ -65,6 +67,16 @@ def create_default_layout(app, message='',
6567

6668
get_prompt_tokens_1, get_prompt_tokens_2 = _split_multiline_prompt(
6769
get_prompt_tokens)
70+
71+
# `lexer` is supposed to be a `Lexer` instance. But if a Pygments lexer
72+
# class is given, turn it into a PygmentsLexer. (Important for
73+
# backwards-compatibility.)
74+
try:
75+
if issubclass(lexer, Lexer):
76+
lexer = PygmentsLexer(lexer)
77+
except TypeError: # Happens when lexer is `None` or an instance of something else.
78+
pass
79+
6880
# Create processors list.
6981
# (DefaultPrompt should always be at the end.)
7082
input_processors = [

tests/unit/test_load_completions.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
import unittest
14+
15+
from awsshell.index.completion import CompletionIndex
16+
17+
18+
class LoadCompletionsTest(unittest.TestCase):
19+
20+
def setUp(self):
21+
self.completion_index = CompletionIndex()
22+
# This would probably be cleaner with a pytest.fixture like
23+
# test_completions.index_data
24+
DATA = (
25+
'{"aws": '
26+
'{"commands": ["devicefarm", "foo"], '
27+
'"arguments": ["--debug", "--endpoint-url"], '
28+
'"children": {"devicefarm": '
29+
'{"commands": ["create-device-pool"], '
30+
'"children": {"create-device-pool": '
31+
'{"commands": [], '
32+
'"arguments": ["--project-arn", "--name"]}}}, '
33+
'"foo": '
34+
'{"commands": ["bar"], '
35+
'"children": {"bar": '
36+
'{"commands": [], "arguments": ["--baz"]}}}}}}'
37+
)
38+
self.completion_index.load_index = lambda x: DATA
39+
self.completion_index.load_completions()
40+
41+
def test_load_completions(self):
42+
assert self.completion_index.commands == [
43+
'devicefarm', 'foo']
44+
assert self.completion_index.subcommands == [
45+
'create-device-pool', 'bar']
46+
assert self.completion_index.global_opts == [
47+
'--debug', '--endpoint-url']
48+
assert self.completion_index.args_opts == set([
49+
'--project-arn', '--name', '--baz'])

0 commit comments

Comments
 (0)