-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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 4 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 |
---|---|---|
|
@@ -8,32 +8,68 @@ | |
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__) | ||
|
||
|
||
class AliasExtensionLoader(AzCommandsLoader): | ||
class AliasCommandLoader(AzCommandsLoader): | ||
|
||
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') | ||
|
||
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) | ||
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. You need to remove the completer for |
||
c.argument('alias_command', options_list=['--command', '-c'], help='The command that the alias points to.') | ||
|
||
|
||
class AliasCache(object): # pylint: disable=too-few-public-methods | ||
|
||
def load_arguments(self, _): # pylint:disable=no-self-use | ||
pass | ||
reserved_commands = [] | ||
|
||
@staticmethod | ||
def cache_reserved_commands(load_cmd_tbl_func): | ||
if not AliasCache.reserved_commands: | ||
AliasCache.reserved_commands = list(load_cmd_tbl_func([]).keys()) | ||
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. What's the point in a class that has only one method, no constructor and even that method is static? 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.
|
||
|
||
|
||
@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 +80,11 @@ 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 _: {}) | ||
AliasCache.cache_reserved_commands(load_cmd_tbl_func) | ||
|
||
elapsed_time = (timeit.default_timer() - start_time) * 1000 | ||
logger.debug(DEBUG_MSG_WITH_TIMING, args, elapsed_time) | ||
|
||
|
@@ -55,4 +96,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 |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
|
||
from knack.log import get_logger | ||
|
||
import azext_alias | ||
from azext_alias import telemetry | ||
from azext_alias._const import ( | ||
GLOBAL_CONFIG_DIR, | ||
|
@@ -56,7 +57,6 @@ def __init__(self, **kwargs): | |
self.alias_table = get_config_parser() | ||
self.kwargs = kwargs | ||
self.collided_alias = defaultdict(list) | ||
self.reserved_commands = [] | ||
self.alias_config_str = '' | ||
self.alias_config_hash = '' | ||
self.load_alias_table() | ||
|
@@ -131,23 +131,26 @@ 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(), | ||
azext_alias.AliasCache.reserved_commands) | ||
else: | ||
self.load_collided_alias() | ||
|
||
transformed_commands = [] | ||
alias_iter = enumerate(args, 1) | ||
|
||
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]): | ||
is_collided_alias = alias in self.collided_alias and alias_index in self.collided_alias[alias] | ||
# Check if the current alias is a named argument | ||
# index - 2 because alias_iter starts counting at index 1 | ||
is_named_arg = alias_index > 1 and args[alias_index - 2].startswith('-') | ||
is_named_arg_flag = alias.startswith('-') | ||
if not alias or is_collided_alias or is_named_arg or is_named_arg_flag: | ||
transformed_commands.append(alias) | ||
continue | ||
|
||
|
@@ -174,35 +177,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. | ||
|
@@ -223,7 +197,7 @@ def load_full_command_table(self): | |
Perform a full load of the command table to get all the reserved command words. | ||
""" | ||
load_cmd_tbl_func = self.kwargs.get('load_cmd_tbl_func', lambda _: {}) | ||
self.reserved_commands = list(load_cmd_tbl_func([]).keys()) | ||
azext_alias.AliasCache.reserved_commands = list(load_cmd_tbl_func([]).keys()) | ||
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. This code is similar to that in |
||
telemetry.set_full_command_table_loaded() | ||
|
||
def post_transform(self, args): | ||
|
@@ -237,15 +211,65 @@ def post_transform(self, args): | |
args = args[1:] if args and args[0] == 'az' else args | ||
|
||
post_transform_commands = [] | ||
for arg in args: | ||
post_transform_commands.append(os.path.expandvars(arg)) | ||
for i, arg in enumerate(args): | ||
# Do not translate environment variables for command argument | ||
if args[:2] == ['alias', 'create'] and i > 0 and args[i - 1] in ['-c', '--command']: | ||
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. Consider a util method called |
||
post_transform_commands.append(arg) | ||
else: | ||
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 +278,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): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
{ | ||
"azext.minCliCoreVersion": "2.0.28" | ||
"azext.minCliCoreVersion": "2.0.28", | ||
"azext.isPreview": true | ||
} |
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.
Why this name change?
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.
Changed it to
AliasExtCommandLoader
.