-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Alias 0.3.0 #105
Alias 0.3.0 #105
Changes from 2 commits
bf4c21d
31c4453
2cebfd3
11128e4
d147667
e87619b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,37 +3,65 @@ | |
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
# pylint: disable=line-too-long | ||
|
||
import timeit | ||
|
||
from knack.log import get_logger | ||
|
||
from azure.cli.core import AzCommandsLoader | ||
from azure.cli.core.commands import CliCommandType | ||
from azure.cli.core.decorators import Completer | ||
from azure.cli.core.commands.events import EVENT_INVOKER_PRE_CMD_TBL_TRUNCATE | ||
from azext_alias.alias import AliasManager | ||
from azext_alias.alias import ( | ||
GLOBAL_ALIAS_PATH, | ||
AliasManager, | ||
get_config_parser | ||
) | ||
from azext_alias._const import DEBUG_MSG_WITH_TIMING | ||
from azext_alias import telemetry | ||
from azext_alias import _help # pylint: disable=unused-import | ||
|
||
logger = get_logger(__name__) | ||
cached_reserved_commands = [] | ||
|
||
|
||
class AliasExtensionLoader(AzCommandsLoader): | ||
class AliasCommandLoader(AzCommandsLoader): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this name change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed it to |
||
|
||
def __init__(self, cli_ctx=None): | ||
super(AliasExtensionLoader, self).__init__(cli_ctx=cli_ctx, | ||
custom_command_type=CliCommandType()) | ||
|
||
from azure.cli.core.commands import CliCommandType | ||
custom_command_type = CliCommandType(operations_tmpl='azext_alias.custom#{}') | ||
super(AliasCommandLoader, self).__init__(cli_ctx=cli_ctx, | ||
custom_command_type=custom_command_type) | ||
self.cli_ctx.register_event(EVENT_INVOKER_PRE_CMD_TBL_TRUNCATE, alias_event_handler) | ||
|
||
def load_command_table(self, _): # pylint:disable=no-self-use | ||
return {} | ||
def load_command_table(self, _): | ||
with self.command_group('alias') as g: | ||
g.custom_command('create', 'create_alias') | ||
g.custom_command('list', 'list_alias') | ||
g.custom_command('remove', 'remove_alias') | ||
|
||
def load_arguments(self, _): # pylint:disable=no-self-use | ||
pass | ||
return self.command_table | ||
|
||
def load_arguments(self, _): | ||
with self.argument_context('alias') as c: | ||
c.argument('alias_name', options_list=['--name', '-n'], help='The name of the alias.', completer=get_alias_completer) | ||
c.argument('alias_command', options_list=['--command', '-c'], help='The command that the alias points to.') | ||
|
||
|
||
@Completer | ||
def get_alias_completer(cmd, prefix, namespace, **kwargs): # pylint: disable=unused-argument | ||
try: | ||
alias_table = get_config_parser() | ||
alias_table.read(GLOBAL_ALIAS_PATH) | ||
return alias_table.sections() | ||
except Exception: # pylint: disable=broad-except | ||
return [] | ||
|
||
|
||
def alias_event_handler(_, **kwargs): | ||
""" An event handler for alias transformation when EVENT_INVOKER_PRE_TRUNCATE_CMD_TBL event is invoked """ | ||
""" | ||
An event handler for alias transformation when EVENT_INVOKER_PRE_TRUNCATE_CMD_TBL event is invoked | ||
""" | ||
try: | ||
telemetry.start() | ||
|
||
|
@@ -44,6 +72,12 @@ def alias_event_handler(_, **kwargs): | |
# [:] will keep the reference of the original args | ||
args[:] = alias_manager.transform(args) | ||
|
||
# Cache the reserved commands for validation later | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you elaborate this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have access to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be good to add this comment as an actual comment in the code. No-one is going to come back and look at this comment in this PR to try and understand it. |
||
if args[:2] == ['alias', 'create']: | ||
load_cmd_tbl_func = kwargs.get('load_cmd_tbl_func', lambda _: {}) | ||
global cached_reserved_commands # pylint: disable=global-statement | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to avoid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a class called |
||
cached_reserved_commands = alias_manager.reserved_commands if alias_manager.reserved_commands else load_cmd_tbl_func([]).keys() | ||
|
||
elapsed_time = (timeit.default_timer() - start_time) * 1000 | ||
logger.debug(DEBUG_MSG_WITH_TIMING, args, elapsed_time) | ||
|
||
|
@@ -55,4 +89,4 @@ def alias_event_handler(_, **kwargs): | |
telemetry.conclude() | ||
|
||
|
||
COMMAND_LOADER_CLS = AliasExtensionLoader | ||
COMMAND_LOADER_CLS = AliasCommandLoader |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
from knack.help_files import helps # pylint: disable=unused-import | ||
|
||
|
||
helps['alias'] = """ | ||
type: group | ||
short-summary: Manage Azure CLI Aliases. | ||
""" | ||
|
||
|
||
helps['alias create'] = """ | ||
type: command | ||
short-summary: Create an alias. | ||
examples: | ||
- name: Create a simple alias. | ||
text: > | ||
az alias create --name rg --command group\n | ||
az alias create --name ls --command list | ||
- name: Create a complex alias. | ||
text: > | ||
az alias create --name list-vm --command 'vm list --resource-group myResourceGroup' | ||
|
||
- name: Create an alias with positional arguments. | ||
text: > | ||
az alias create --name 'list-vm {{ resource_group }}' --command 'vm list --resource-group {{ resource_group }}' | ||
|
||
- name: Create an alias with positional arguments and additional string processing. | ||
text: > | ||
az alias create --name 'storage-ls {{ url }}' --command 'storage blob list \n | ||
--account-name {{ url.replace("https://", "").split(".")[0] }}\n | ||
--container-name {{ url.replace("https://", "").split("/")[1] }}' | ||
""" | ||
|
||
|
||
helps['alias list'] = """ | ||
type: command | ||
short-summary: List the registered aliases. | ||
""" | ||
|
||
|
||
helps['alias remove'] = """ | ||
type: command | ||
short-summary: Remove an alias. | ||
""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -131,25 +131,29 @@ def transform(self, args): | |
""" | ||
if self.parse_error(): | ||
# Write an empty hash so next run will check the config file against the entire command table again | ||
self.write_alias_config_hash(True) | ||
AliasManager.write_alias_config_hash(empty_hash=True) | ||
return args | ||
|
||
# Only load the entire command table if it detects changes in the alias config | ||
if self.detect_alias_config_change(): | ||
self.load_full_command_table() | ||
self.build_collision_table() | ||
self.collided_alias = AliasManager.build_collision_table(self.alias_table.sections(), | ||
self.reserved_commands) | ||
else: | ||
self.load_collided_alias() | ||
|
||
transformed_commands = [] | ||
alias_iter = enumerate(args, 1) | ||
|
||
ignore_next_iter = False | ||
for alias_index, alias in alias_iter: | ||
# Directly append invalid alias or collided alias | ||
if not alias or alias[0] == '-' or (alias in self.collided_alias and | ||
alias_index in self.collided_alias[alias]): | ||
if not alias or alias[0] == '-' or ignore_next_iter or (alias in self.collided_alias and | ||
alias_index in self.collided_alias[alias]): | ||
transformed_commands.append(alias) | ||
ignore_next_iter = alias and alias[0] == '-' | ||
continue | ||
else: | ||
ignore_next_iter = False | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you elaborate the logic here in the comments? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here is to not transform named arguments. For example, if you have an alias |
||
|
||
full_alias = self.get_full_alias(alias) | ||
|
||
|
@@ -174,35 +178,6 @@ def transform(self, args): | |
|
||
return self.post_transform(transformed_commands) | ||
|
||
def build_collision_table(self, levels=COLLISION_CHECK_LEVEL_DEPTH): | ||
""" | ||
Build the collision table according to the alias configuration file against the entire command table. | ||
|
||
self.collided_alias is structured as: | ||
{ | ||
'collided_alias': [the command level at which collision happens] | ||
} | ||
For example: | ||
{ | ||
'account': [1, 2] | ||
} | ||
This means that 'account' is a reserved command in level 1 and level 2 of the command tree because | ||
(az account ...) and (az storage account ...) | ||
lvl 1 lvl 2 | ||
|
||
Args: | ||
levels: the amount of levels we tranverse through the command table tree. | ||
""" | ||
for alias in self.alias_table.sections(): | ||
# Only care about the first word in the alias because alias | ||
# cannot have spaces (unless they have positional arguments) | ||
word = alias.split()[0] | ||
for level in range(1, levels + 1): | ||
collision_regex = r'^{}{}($|\s)'.format(r'([a-z\-]*\s)' * (level - 1), word.lower()) | ||
if list(filter(re.compile(collision_regex).match, self.reserved_commands)): | ||
self.collided_alias[word].append(level) | ||
telemetry.set_collided_aliases(list(self.collided_alias.keys())) | ||
|
||
def get_full_alias(self, query): | ||
""" | ||
Get the full alias given a search query. | ||
|
@@ -240,12 +215,58 @@ def post_transform(self, args): | |
for arg in args: | ||
post_transform_commands.append(os.path.expandvars(arg)) | ||
|
||
self.write_alias_config_hash() | ||
self.write_collided_alias() | ||
AliasManager.write_alias_config_hash(self.alias_config_hash) | ||
AliasManager.write_collided_alias(self.collided_alias) | ||
|
||
return post_transform_commands | ||
|
||
def write_alias_config_hash(self, empty_hash=False): | ||
def parse_error(self): | ||
""" | ||
Check if there is a configuration parsing error. | ||
|
||
A parsing error has occurred if there are strings inside the alias config file | ||
but there is no alias loaded in self.alias_table. | ||
|
||
Returns: | ||
True if there is an error parsing the alias configuration file. Otherwises, false. | ||
""" | ||
return not self.alias_table.sections() and self.alias_config_str | ||
|
||
@staticmethod | ||
def build_collision_table(aliases, reserved_commands, levels=COLLISION_CHECK_LEVEL_DEPTH): | ||
""" | ||
Build the collision table according to the alias configuration file against the entire command table. | ||
|
||
self.collided_alias is structured as: | ||
{ | ||
'collided_alias': [the command level at which collision happens] | ||
} | ||
For example: | ||
{ | ||
'account': [1, 2] | ||
} | ||
This means that 'account' is a reserved command in level 1 and level 2 of the command tree because | ||
(az account ...) and (az storage account ...) | ||
lvl 1 lvl 2 | ||
|
||
Args: | ||
levels: the amount of levels we tranverse through the command table tree. | ||
""" | ||
collided_alias = defaultdict(list) | ||
for alias in aliases: | ||
# Only care about the first word in the alias because alias | ||
# cannot have spaces (unless they have positional arguments) | ||
word = alias.split()[0] | ||
for level in range(1, levels + 1): | ||
collision_regex = r'^{}{}($|\s)'.format(r'([a-z\-]*\s)' * (level - 1), word.lower()) | ||
if list(filter(re.compile(collision_regex).match, reserved_commands)): | ||
collided_alias[word].append(level) | ||
|
||
telemetry.set_collided_aliases(list(collided_alias.keys())) | ||
return collided_alias | ||
|
||
@staticmethod | ||
def write_alias_config_hash(alias_config_hash='', empty_hash=False): | ||
""" | ||
Write self.alias_config_hash to the alias hash file. | ||
|
||
|
@@ -254,29 +275,18 @@ def write_alias_config_hash(self, empty_hash=False): | |
means that we have to perform a full load of the command table in the next run. | ||
""" | ||
with open(GLOBAL_ALIAS_HASH_PATH, 'w') as alias_config_hash_file: | ||
alias_config_hash_file.write('' if empty_hash else self.alias_config_hash) | ||
alias_config_hash_file.write('' if empty_hash else alias_config_hash) | ||
|
||
def write_collided_alias(self): | ||
@staticmethod | ||
def write_collided_alias(collided_alias_dict): | ||
""" | ||
Write the collided aliases string into the collided alias file. | ||
""" | ||
# w+ creates the alias config file if it does not exist | ||
open_mode = 'r+' if os.path.exists(GLOBAL_COLLIDED_ALIAS_PATH) else 'w+' | ||
with open(GLOBAL_COLLIDED_ALIAS_PATH, open_mode) as collided_alias_file: | ||
collided_alias_file.truncate() | ||
collided_alias_file.write(json.dumps(self.collided_alias)) | ||
|
||
def parse_error(self): | ||
""" | ||
Check if there is a configuration parsing error. | ||
|
||
A parsing error has occurred if there are strings inside the alias config file | ||
but there is no alias loaded in self.alias_table. | ||
|
||
Returns: | ||
True if there is an error parsing the alias configuration file. Otherwises, false. | ||
""" | ||
return not self.alias_table.sections() and self.alias_config_str | ||
collided_alias_file.write(json.dumps(collided_alias_dict)) | ||
|
||
@staticmethod | ||
def process_exception_message(exception): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this. You can break the line to fix in 120 char.