diff --git a/src/alias/azext_alias/__init__.py b/src/alias/azext_alias/__init__.py index c602353d406..c289ecdaa74 100644 --- a/src/alias/azext_alias/__init__.py +++ b/src/alias/azext_alias/__init__.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from argcomplete.completers import FilesCompleter # pylint: disable=import-error + from azure.cli.core import AzCommandsLoader from azure.cli.core.decorators import Completer from azure.cli.core.commands.events import EVENT_INVOKER_PRE_CMD_TBL_TRUNCATE, EVENT_INVOKER_ON_TAB_COMPLETION @@ -10,6 +12,13 @@ EVENT_INTERACTIVE_PRE_COMPLETER_TEXT_PARSING, EVENT_INTERACTIVE_POST_SUB_TREE_CREATE ) + +from azext_alias.util import get_alias_table +from azext_alias._validators import ( + process_alias_create_namespace, + process_alias_import_namespace, + process_alias_export_namespace +) from azext_alias import _help # pylint: disable=unused-import from azext_alias.hooks import ( alias_event_handler, @@ -17,8 +26,6 @@ transform_cur_commands_interactive, enable_aliases_autocomplete_interactive ) -from azext_alias.util import get_alias_table -from azext_alias._validators import process_alias_create_namespace # We don't have access to load_cmd_tbl_func in custom.py (need the entire command table @@ -41,10 +48,15 @@ def __init__(self, cli_ctx=None): self.cli_ctx.register_event(EVENT_INTERACTIVE_POST_SUB_TREE_CREATE, enable_aliases_autocomplete_interactive) def load_command_table(self, _): + with self.command_group('alias') as g: g.custom_command('create', 'create_alias', validator=process_alias_create_namespace) + g.custom_command('export', 'export_aliases', validator=process_alias_export_namespace) + g.custom_command('import', 'import_aliases', validator=process_alias_import_namespace) g.custom_command('list', 'list_alias') g.custom_command('remove', 'remove_alias') + g.custom_command('remove-all', 'remove_all_aliases', + confirmation='Are you sure you want to remove all registered aliases?') return self.command_table @@ -53,9 +65,19 @@ def load_arguments(self, _): c.argument('alias_name', options_list=['--name', '-n'], help='The name of the alias.') c.argument('alias_command', options_list=['--command', '-c'], help='The command that the alias points to.') + with self.argument_context('alias export') as c: + c.argument('export_path', options_list=['--path', '-p'], + help='The path of the alias configuration file to export to', completer=FilesCompleter()) + c.argument('exclusions', options_list=['--exclude', '-e'], + help='Space-separated aliases excluded from export', completer=get_alias_completer, nargs='*') + + with self.argument_context('alias import') as c: + c.argument('alias_source', options_list=['--source', '-s'], + help='The source of the aliases to import from.', completer=FilesCompleter()) + with self.argument_context('alias remove') as c: - c.argument('alias_name', options_list=['--name', '-n'], help='The name of the alias.', - completer=get_alias_completer) + c.argument('alias_names', options_list=['--name', '-n'], help='Space-separated aliases', + completer=get_alias_completer, nargs='*') @Completer diff --git a/src/alias/azext_alias/_const.py b/src/alias/azext_alias/_const.py index 109ca3fd093..6581c5d0319 100644 --- a/src/alias/azext_alias/_const.py +++ b/src/alias/azext_alias/_const.py @@ -16,13 +16,13 @@ COLLISION_CHECK_LEVEL_DEPTH = 5 INSUFFICIENT_POS_ARG_ERROR = 'alias: "{}" takes exactly {} positional argument{} ({} given)' -CONFIG_PARSING_ERROR = 'alias: Error parsing the configuration file - %s. Please fix the problem manually.' +CONFIG_PARSING_ERROR = 'alias: Please ensure you have a valid alias configuration file. Error detail: %s' DEBUG_MSG = 'Alias Manager: Transforming "%s" to "%s"' DEBUG_MSG_WITH_TIMING = 'Alias Manager: Transformed args to %s in %.3fms' POS_ARG_DEBUG_MSG = 'Alias Manager: Transforming "%s" to "%s", with the following positional arguments: %s' DUPLICATED_PLACEHOLDER_ERROR = 'alias: Duplicated placeholders found when transforming "{}"' -RENDER_TEMPLATE_ERROR = 'alias: Encounted the following error when injecting positional arguments to "{}" - {}' -PLACEHOLDER_EVAL_ERROR = 'alias: Encounted the following error when evaluating "{}" - {}' +RENDER_TEMPLATE_ERROR = 'alias: Encounted error when injecting positional arguments to "{}". Error detail: {}' +PLACEHOLDER_EVAL_ERROR = 'alias: Encounted error when evaluating "{}". Error detail: {}' PLACEHOLDER_BRACKETS_ERROR = 'alias: Brackets in "{}" are not enclosed properly' ALIAS_NOT_FOUND_ERROR = 'alias: "{}" alias not found' INVALID_ALIAS_COMMAND_ERROR = 'alias: Invalid Azure CLI command "{}"' @@ -30,3 +30,8 @@ INVALID_STARTING_CHAR_ERROR = 'alias: Alias name should not start with "{}"' INCONSISTENT_ARG_ERROR = 'alias: Positional argument{} {} {} not in both alias name and alias command' COMMAND_LVL_ERROR = 'alias: "{}" is a reserved command and cannot be used to represent "{}"' +ALIAS_FILE_NOT_FOUND_ERROR = 'alias: File not found' +ALIAS_FILE_DIR_ERROR = 'alias: {} is a directory' +ALIAS_FILE_URL_ERROR = 'alias: Encounted error when retrieving alias file from {}. Error detail: {}' +POST_EXPORT_ALIAS_MSG = 'alias: Exported alias configuration file to %s.' +FILE_ALREADY_EXISTS_ERROR = 'alias: {} already exists.' diff --git a/src/alias/azext_alias/_help.py b/src/alias/azext_alias/_help.py index 0710020ee84..d4b2cbe0b3d 100644 --- a/src/alias/azext_alias/_help.py +++ b/src/alias/azext_alias/_help.py @@ -39,6 +39,18 @@ """ +helps['alias export'] = """ + type: command + short-summary: Export all registered aliases to a given path, as an INI configuration file. +""" + + +helps['alias import'] = """ + type: command + short-summary: Import aliases from an INI configuration file or an URL. +""" + + helps['alias list'] = """ type: command short-summary: List the registered aliases. @@ -47,5 +59,11 @@ helps['alias remove'] = """ type: command - short-summary: Remove an alias. + short-summary: Remove one or more aliases. Aliases to be removed are space-delimited. +""" + + +helps['alias remove-all'] = """ + type: command + short-summary: Remove all registered aliases. """ diff --git a/src/alias/azext_alias/_validators.py b/src/alias/azext_alias/_validators.py index b8f8627db10..fbd4eb23cf8 100644 --- a/src/alias/azext_alias/_validators.py +++ b/src/alias/azext_alias/_validators.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import os import re import shlex @@ -10,13 +11,25 @@ import azext_alias from azext_alias.argument import get_placeholders +from azext_alias.util import ( + get_config_parser, + is_url, + reduce_alias_table, + filter_alias_create_namespace, + retrieve_file_from_url +) from azext_alias._const import ( COLLISION_CHECK_LEVEL_DEPTH, INVALID_ALIAS_COMMAND_ERROR, EMPTY_ALIAS_ERROR, INVALID_STARTING_CHAR_ERROR, INCONSISTENT_ARG_ERROR, - COMMAND_LVL_ERROR + COMMAND_LVL_ERROR, + CONFIG_PARSING_ERROR, + ALIAS_FILE_NOT_FOUND_ERROR, + ALIAS_FILE_DIR_ERROR, + FILE_ALREADY_EXISTS_ERROR, + ALIAS_FILE_NAME ) from azext_alias.alias import AliasManager @@ -28,12 +41,49 @@ def process_alias_create_namespace(namespace): Args: namespace: argparse namespace object. """ + namespace = filter_alias_create_namespace(namespace) _validate_alias_name(namespace.alias_name) _validate_alias_command(namespace.alias_command) _validate_alias_command_level(namespace.alias_name, namespace.alias_command) _validate_pos_args_syntax(namespace.alias_name, namespace.alias_command) +def process_alias_import_namespace(namespace): + """ + Validate input arguments when the user invokes 'az alias import'. + + Args: + namespace: argparse namespace object. + """ + if is_url(namespace.alias_source): + alias_source = retrieve_file_from_url(namespace.alias_source) + + _validate_alias_file_content(alias_source, url=namespace.alias_source) + else: + namespace.alias_source = os.path.abspath(namespace.alias_source) + _validate_alias_file_path(namespace.alias_source) + _validate_alias_file_content(namespace.alias_source) + + +def process_alias_export_namespace(namespace): + """ + Validate input arguments when the user invokes 'az alias export'. + + Args: + namespace: argparse namespace object. + """ + namespace.export_path = os.path.abspath(namespace.export_path) + if os.path.isfile(namespace.export_path): + raise CLIError(FILE_ALREADY_EXISTS_ERROR.format(namespace.export_path)) + + export_path_dir = os.path.dirname(namespace.export_path) + if not os.path.isdir(export_path_dir): + os.makedirs(export_path_dir) + + if os.path.isdir(namespace.export_path): + namespace.export_path = os.path.join(namespace.export_path, ALIAS_FILE_NAME) + + def _validate_alias_name(alias_name): """ Check if the alias name is valid. @@ -58,7 +108,6 @@ def _validate_alias_command(alias_command): if not alias_command: raise CLIError(EMPTY_ALIAS_ERROR) - # Boundary index is the index at which named argument or positional argument starts split_command = shlex.split(alias_command) boundary_index = len(split_command) for i, subcommand in enumerate(split_command): @@ -72,7 +121,7 @@ def _validate_alias_command(alias_command): if re.match(r'([a-z\-]*\s)*{}($|\s)'.format(command_to_validate), command): return - raise CLIError(INVALID_ALIAS_COMMAND_ERROR.format(command_to_validate if command_to_validate else alias_command)) + _validate_positional_arguments(shlex.split(alias_command)) def _validate_pos_args_syntax(alias_name, alias_command): @@ -120,3 +169,71 @@ def _validate_alias_command_level(alias, command): # Check if there is a command level conflict if set(alias_collision_levels) & set(command_collision_levels): raise CLIError(COMMAND_LVL_ERROR.format(alias, command)) + + +def _validate_alias_file_path(alias_file_path): + """ + Make sure the alias file path is neither non-existant nor a directory + + Args: + The alias file path to import aliases from. + """ + if not os.path.exists(alias_file_path): + raise CLIError(ALIAS_FILE_NOT_FOUND_ERROR) + + if os.path.isdir(alias_file_path): + raise CLIError(ALIAS_FILE_DIR_ERROR.format(alias_file_path)) + + +def _validate_alias_file_content(alias_file_path, url=''): + """ + Make sure the alias name and alias command in the alias file is in valid format. + + Args: + The alias file path to import aliases from. + """ + alias_table = get_config_parser() + try: + alias_table.read(alias_file_path) + for alias_name, alias_command in reduce_alias_table(alias_table): + _validate_alias_name(alias_name) + _validate_alias_command(alias_command) + _validate_alias_command_level(alias_name, alias_command) + _validate_pos_args_syntax(alias_name, alias_command) + except Exception as exception: # pylint: disable=broad-except + error_msg = CONFIG_PARSING_ERROR % AliasManager.process_exception_message(exception) + error_msg = error_msg.replace(alias_file_path, url or alias_file_path) + raise CLIError(error_msg) + + +def _validate_positional_arguments(args): + """ + To validate the positional argument feature - https://github.com/Azure/azure-cli/pull/6055. + Assuming that unknown commands are positional arguments immediately + led by words that only appear at the end of the commands + + Slight modification of + https://github.com/Azure/azure-cli/blob/dev/src/azure-cli-core/azure/cli/core/commands/__init__.py#L356-L373 + + Args: + args: The arguments that the user inputs in the terminal. + + Returns: + Rudimentary parsed arguments. + """ + nouns = [] + for arg in args: + if not arg.startswith('-') or not arg.startswith('{{'): + nouns.append(arg) + else: + break + + while nouns: + search = ' '.join(nouns) + # Since the command name may be immediately followed by a positional arg, strip those off + if not next((x for x in azext_alias.cached_reserved_commands if x.endswith(search)), False): + del nouns[-1] + else: + return + + raise CLIError(INVALID_ALIAS_COMMAND_ERROR.format(' '.join(args))) diff --git a/src/alias/azext_alias/alias.py b/src/alias/azext_alias/alias.py index a7927d7bc7c..1316f5853c6 100644 --- a/src/alias/azext_alias/alias.py +++ b/src/alias/azext_alias/alias.py @@ -26,7 +26,7 @@ ) from azext_alias.argument import build_pos_args_table, render_template from azext_alias.util import ( - is_alias_create_command, + is_alias_command, cache_reserved_commands, get_config_parser, build_tab_completion_table @@ -139,7 +139,8 @@ def transform(self, args): # 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: + excluded_commands = is_alias_command(['remove', 'export'], transformed_commands) + if not alias or is_collided_alias or is_named_arg or is_named_arg_flag or excluded_commands: transformed_commands.append(alias) continue @@ -202,7 +203,7 @@ def post_transform(self, args): post_transform_commands = [] for i, arg in enumerate(args): # Do not translate environment variables for command argument - if is_alias_create_command(args) and i > 0 and args[i - 1] in ['-c', '--command']: + if is_alias_command(['create'], args) and i > 0 and args[i - 1] in ['-c', '--command']: post_transform_commands.append(arg) else: post_transform_commands.append(os.path.expandvars(arg)) diff --git a/src/alias/azext_alias/custom.py b/src/alias/azext_alias/custom.py index 09b1a77ed09..83fd47b9937 100644 --- a/src/alias/azext_alias/custom.py +++ b/src/alias/azext_alias/custom.py @@ -3,13 +3,23 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import os import hashlib from knack.util import CLIError +from knack.log import get_logger -from azext_alias._const import ALIAS_NOT_FOUND_ERROR +from azext_alias._const import ALIAS_NOT_FOUND_ERROR, POST_EXPORT_ALIAS_MSG, ALIAS_FILE_NAME from azext_alias.alias import GLOBAL_ALIAS_PATH, AliasManager -from azext_alias.util import get_alias_table, build_tab_completion_table +from azext_alias.util import ( + get_alias_table, + is_url, + build_tab_completion_table, + get_config_parser, + retrieve_file_from_url +) + +logger = get_logger(__name__) def create_alias(alias_name, alias_command): @@ -29,6 +39,41 @@ def create_alias(alias_name, alias_command): _commit_change(alias_table) +def export_aliases(export_path=os.path.abspath(ALIAS_FILE_NAME), exclusions=None): + """ + Export all registered aliases to a given path, as an INI configuration file. + + Args: + export_path: The path of the alias configuration file to export to. + exclusions: Space-separated aliases excluded from export. + """ + alias_table = get_alias_table() + for exclusion in exclusions or []: + if exclusion not in alias_table.sections(): + raise CLIError(ALIAS_NOT_FOUND_ERROR.format(exclusion)) + alias_table.remove_section(exclusion) + + _commit_change(alias_table, export_path=export_path, post_commit=False) + logger.warning(POST_EXPORT_ALIAS_MSG, export_path) # pylint: disable=superfluous-parens + + +def import_aliases(alias_source): + """ + Import aliases from a file or an URL. + + Args: + alias_source: The source of the alias. It can be a filepath or an URL. + """ + alias_table = get_alias_table() + if is_url(alias_source): + alias_source = retrieve_file_from_url(alias_source) + alias_table.read(alias_source) + os.remove(alias_source) + else: + alias_table.read(alias_source) + _commit_change(alias_table) + + def list_alias(): """ List all registered aliases. @@ -49,7 +94,7 @@ def list_alias(): return output -def remove_alias(alias_name): +def remove_alias(alias_names): """ Remove an alias. @@ -57,26 +102,36 @@ def remove_alias(alias_name): alias_name: The name of the alias to be removed. """ alias_table = get_alias_table() - if alias_name not in alias_table.sections(): - raise CLIError(ALIAS_NOT_FOUND_ERROR.format(alias_name)) - alias_table.remove_section(alias_name) + for alias_name in alias_names: + if alias_name not in alias_table.sections(): + raise CLIError(ALIAS_NOT_FOUND_ERROR.format(alias_name)) + alias_table.remove_section(alias_name) _commit_change(alias_table) -def _commit_change(alias_table): +def remove_all_aliases(): + """ + Remove all registered aliases. + """ + _commit_change(get_config_parser()) + + +def _commit_change(alias_table, export_path=None, post_commit=True): """ Record changes to the alias table. Also write new alias config hash and collided alias, if any. Args: alias_table: The alias table to commit. + export_path: The path to export the aliases to. Default: GLOBAL_ALIAS_PATH. + post_commit: True if we want to perform some extra actions after writing alias to file. """ - with open(GLOBAL_ALIAS_PATH, 'w+') as alias_config_file: + with open(export_path or GLOBAL_ALIAS_PATH, 'w+') as alias_config_file: alias_table.write(alias_config_file) - alias_config_file.seek(0) - alias_config_hash = hashlib.sha1(alias_config_file.read().encode('utf-8')).hexdigest() - AliasManager.write_alias_config_hash(alias_config_hash) - - collided_alias = AliasManager.build_collision_table(alias_table.sections()) - AliasManager.write_collided_alias(collided_alias) - build_tab_completion_table(alias_table) + if post_commit: + alias_config_file.seek(0) + alias_config_hash = hashlib.sha1(alias_config_file.read().encode('utf-8')).hexdigest() + AliasManager.write_alias_config_hash(alias_config_hash) + collided_alias = AliasManager.build_collision_table(alias_table.sections()) + AliasManager.write_collided_alias(collided_alias) + build_tab_completion_table(alias_table) diff --git a/src/alias/azext_alias/hooks.py b/src/alias/azext_alias/hooks.py index 5e3b49c4cd6..e5b601b6a14 100644 --- a/src/alias/azext_alias/hooks.py +++ b/src/alias/azext_alias/hooks.py @@ -12,7 +12,7 @@ from azext_alias import telemetry from azext_alias.alias import AliasManager from azext_alias.util import ( - is_alias_create_command, + is_alias_command, cache_reserved_commands, get_alias_table, filter_aliases @@ -36,7 +36,7 @@ def alias_event_handler(_, **kwargs): # [:] will keep the reference of the original args args[:] = alias_manager.transform(args) - if is_alias_create_command(args): + if is_alias_command(['create', 'import'], args): load_cmd_tbl_func = kwargs.get('load_cmd_tbl_func', lambda _: {}) cache_reserved_commands(load_cmd_tbl_func) diff --git a/src/alias/azext_alias/tests/_const.py b/src/alias/azext_alias/tests/_const.py index c55cfdcea49..6075050cc93 100644 --- a/src/alias/azext_alias/tests/_const.py +++ b/src/alias/azext_alias/tests/_const.py @@ -84,4 +84,5 @@ TEST_RESERVED_COMMANDS = ['account list-locations', 'network dns', - 'storage account create'] + 'storage account create', + 'group delete'] diff --git a/src/alias/azext_alias/tests/test_alias.py b/src/alias/azext_alias/tests/test_alias.py index b4f06a831cb..d591161ecce 100644 --- a/src/alias/azext_alias/tests/test_alias.py +++ b/src/alias/azext_alias/tests/test_alias.py @@ -9,7 +9,7 @@ import sys import shlex import unittest -from mock import Mock +from mock import Mock, patch from six.moves import configparser from knack.util import CLIError @@ -91,7 +91,7 @@ def test_transform_alias(self, test_case): def test_transform_collided_alias(self, test_case): - alias_manager = self.get_alias_manager(COLLISION_MOCK_ALIAS_STRING, TEST_RESERVED_COMMANDS) + alias_manager = self.get_alias_manager(COLLISION_MOCK_ALIAS_STRING) alias_manager.collided_alias = azext_alias.alias.AliasManager.build_collision_table(alias_manager.alias_table.sections()) self.assertEqual(shlex.split(test_case[1]), alias_manager.transform(shlex.split(test_case[0]))) @@ -147,18 +147,22 @@ def test(self): class TestAlias(unittest.TestCase): - @classmethod - def setUp(cls): + def setUp(self): azext_alias.alias.AliasManager.write_alias_config_hash = Mock() azext_alias.alias.AliasManager.write_collided_alias = Mock() + self.patcher = patch('azext_alias.cached_reserved_commands', TEST_RESERVED_COMMANDS) + self.patcher.start() + + def tearDown(self): + self.patcher.stop() def test_build_empty_collision_table(self): - alias_manager = self.get_alias_manager(DEFAULT_MOCK_ALIAS_STRING, TEST_RESERVED_COMMANDS) + alias_manager = self.get_alias_manager(DEFAULT_MOCK_ALIAS_STRING) test_case = azext_alias.alias.AliasManager.build_collision_table(alias_manager.alias_table.sections()) self.assertDictEqual(dict(), test_case) def test_build_non_empty_collision_table(self): - alias_manager = self.get_alias_manager(COLLISION_MOCK_ALIAS_STRING, TEST_RESERVED_COMMANDS) + alias_manager = self.get_alias_manager(COLLISION_MOCK_ALIAS_STRING) test_case = azext_alias.alias.AliasManager.build_collision_table(alias_manager.alias_table.sections(), levels=2) self.assertDictEqual({'account': [1, 2], 'dns': [2], 'list-locations': [2]}, test_case) @@ -179,9 +183,8 @@ def test_detect_alias_config_change(self): """ Helper functions """ - def get_alias_manager(self, mock_alias_str=DEFAULT_MOCK_ALIAS_STRING, reserved_commands=None): + def get_alias_manager(self, mock_alias_str=DEFAULT_MOCK_ALIAS_STRING): alias_manager = MockAliasManager(mock_alias_str=mock_alias_str) - azext_alias.cached_reserved_commands = reserved_commands if reserved_commands else [] return alias_manager def assertAlias(self, value): diff --git a/src/alias/azext_alias/tests/test_alias_commands.py b/src/alias/azext_alias/tests/test_alias_commands.py index 74cf6247838..3e9ef8a5488 100644 --- a/src/alias/azext_alias/tests/test_alias_commands.py +++ b/src/alias/azext_alias/tests/test_alias_commands.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# pylint: disable=line-too-long +# pylint: disable=line-too-long,too-many-public-methods import os import shutil @@ -32,6 +32,7 @@ def setUp(self): self.patchers.append(mock.patch('azext_alias.alias.GLOBAL_COLLIDED_ALIAS_PATH', os.path.join(self.mock_config_dir, COLLIDED_ALIAS_FILE_NAME))) self.patchers.append(mock.patch('azext_alias.util.GLOBAL_ALIAS_TAB_COMP_TABLE_PATH', os.path.join(self.mock_config_dir, ALIAS_TAB_COMP_TABLE_FILE_NAME))) self.patchers.append(mock.patch('azext_alias.custom.GLOBAL_ALIAS_PATH', os.path.join(self.mock_config_dir, ALIAS_FILE_NAME))) + os.makedirs(os.path.join(self.mock_config_dir, 'export')) for patcher in self.patchers: patcher.start() @@ -78,6 +79,25 @@ def test_remove_alias(self): self.check('length(@)', 0) ]) + def test_remove_multiple_aliases(self): + self.kwargs.update({ + 'alias_name': 'c', + 'alias_command': 'create' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.kwargs.update({ + 'alias_name': 'storage-ls {{ url }}', + 'alias_command': 'storage blob list --account-name {{ url.replace("https://", "").split(".")[0] }} --container-name {{ url.replace("https://", "").split("/")[1] }}' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.cmd('az alias list', checks=[ + self.check('length(@)', 2) + ]) + self.cmd('az alias remove -n \'storage-ls {{{{ url }}}}\' c') + self.cmd('az alias list', checks=[ + self.check('length(@)', 0) + ]) + def test_remove_alias_non_existing(self): self.kwargs.update({ 'alias_name': 'c', @@ -100,7 +120,7 @@ def test_alias_file_and_hash_create(self): with open(alias.GLOBAL_ALIAS_PATH) as alias_config_file: assert alias_config_file.read() == expected_alias_string - def test_alias_file_and_hash_remove(self): + def test_alias_file_remove(self): self.kwargs.update({ 'alias_name': 'c', 'alias_command': 'create' @@ -116,6 +136,177 @@ def test_alias_file_and_hash_remove(self): with open(alias.GLOBAL_ALIAS_PATH) as alias_config_file: assert not alias_config_file.read() + def test_create_and_import_file(self): + _, mock_alias_config_file = tempfile.mkstemp() + with open(mock_alias_config_file, 'w') as f: + f.write('[c]\ncommand = create\n[grp]\ncommand = group') + + self.kwargs.update({ + 'alias_source': mock_alias_config_file + }) + self.cmd('az alias import -s {alias_source}') + self.cmd('az alias list', checks=[ + self.check('[0].alias', 'c'), + self.check('[0].command', 'create'), + self.check('[1].alias', 'grp'), + self.check('[1].command', 'group'), + self.check('length(@)', 2) + ]) + os.remove(mock_alias_config_file) + + def test_create_and_import_url(self): + self.kwargs.update({ + 'alias_source': 'https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/alias' + }) + self.cmd('az alias import -s {alias_source}') + self.cmd('az alias list', checks=[ + self.check('[0].alias', 'c'), + self.check('[0].command', 'create'), + self.check('[1].alias', 'grp'), + self.check('[1].command', 'group'), + self.check('length(@)', 2) + ]) + + def test_create_and_import_collide(self): + self.kwargs.update({ + 'alias_name': 'c', + 'alias_command': 'vm' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.cmd('az alias list', checks=[ + self.check('[0].alias', '{alias_name}'), + self.check('[0].command', '{alias_command}'), + self.check('length(@)', 1) + ]) + self.kwargs.update({ + 'alias_source': 'https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/alias' + }) + self.cmd('az alias import -s {alias_source}') + self.cmd('az alias list', checks=[ + self.check('[0].alias', 'c'), + self.check('[0].command', 'create'), + self.check('[1].alias', 'grp'), + self.check('[1].command', 'group'), + self.check('length(@)', 2) + ]) + + def test_import_invalid_content_from_url(self): + self.kwargs.update({ + 'alias_source': 'https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/invalid_alias' + }) + self.cmd('az alias import -s {alias_source}', expect_failure=True) + self.cmd('az alias list', checks=[ + self.check('length(@)', 0) + ]) + + def test_remove_all_aliases(self): + self.kwargs.update({ + 'alias_name': 'list-vm {{ resource_group }}', + 'alias_command': 'vm list --resource-group {{ resource_group }}' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.kwargs.update({ + 'alias_name': 'storage-ls {{ url }}', + 'alias_command': 'storage blob list --account-name {{ url.replace("https://", "").split(".")[0] }} --container-name {{ url.replace("https://", "").split("/")[1] }}' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.cmd('az alias list', checks=[ + self.check('length(@)', 2) + ]) + self.cmd('az alias remove-all --yes') + self.cmd('az alias list', checks=[ + self.check('length(@)', 0) + ]) + + def test_excessive_whitespaces_in_alias_command(self): + self.kwargs.update({ + 'alias_name': ' list-vm \n{{ resource_group }} ', + 'alias_command': ' vm \n list --resource-group {{ resource_group }} ' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.cmd('az alias list', checks=[ + self.check('[0].alias', 'list-vm {{{{ resource_group }}}}'), + self.check('[0].command', 'vm list --resource-group {{{{ resource_group }}}}'), + self.check('length(@)', 1) + ]) + + @mock.patch('os.getcwd') + def test_export_file_name_only(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p alias') + self._post_test_export(os.path.join(self.mock_config_dir, 'export', 'alias')) + + @mock.patch('os.getcwd') + def test_export_existing_file(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p alias') + self.cmd('az alias export -p alias', expect_failure=True) + + @mock.patch('os.getcwd') + def test_export_path_relative_path(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p test1/test2/alias') + self._post_test_export(os.path.join(self.mock_config_dir, 'export', 'test1', 'test2', 'alias')) + + @mock.patch('os.getcwd') + def test_export_path_dir_only(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p {}'.format(os.path.join(self.mock_config_dir, 'export'))) + self._post_test_export(os.path.join(self.mock_config_dir, 'export', 'alias')) + + @mock.patch('os.getcwd') + def test_export_path_absolute_path(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p {}'.format(os.path.join(self.mock_config_dir, 'export', 'alias12345'))) + self._post_test_export(os.path.join(self.mock_config_dir, 'export', 'alias12345')) + + @mock.patch('os.getcwd') + def test_export_path_exclusion(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p {} -e \'{}\''.format('alias', 'storage-ls {{{{ url }}}}')) + self._post_test_export(os.path.join(self.mock_config_dir, 'export', 'alias'), test_exclusion=True) + + @mock.patch('os.getcwd') + def test_export_path_exclusion_error(self, mock_os_getcwd): + mock_os_getcwd.return_value = os.path.join(self.mock_config_dir, 'export') + self._pre_test_export() + self.cmd('az alias export -p {} -e {}'.format('alias', 'invalid_alias'), expect_failure=True) + + def _pre_test_export(self): + self.kwargs.update({ + 'alias_name': 'c', + 'alias_command': 'create' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.kwargs.update({ + 'alias_name': 'storage-ls {{ url }}', + 'alias_command': 'storage blob list --account-name {{ url.replace("https://", "").split(".")[0] }} --container-name {{ url.replace("https://", "").split("/")[1] }}' + }) + self.cmd('az alias create -n \'{alias_name}\' -c \'{alias_command}\'') + self.cmd('az alias list', checks=[ + self.check('length(@)', 2) + ]) + + def _post_test_export(self, export_path, test_exclusion=False): # pylint: disable=no-self-use + with open(export_path, 'r') as f: + expected = '''[c] +command = create + +[storage-ls {{ url }}] +command = storage blob list --account-name {{ url.replace("https://", "").split(".")[0] }} --container-name {{ url.replace("https://", "").split("/")[1] }} + +''' if not test_exclusion else '''[c] +command = create + +''' + assert f.read() == expected + if __name__ == '__main__': unittest.main() diff --git a/src/alias/azext_alias/tests/test_argument.py b/src/alias/azext_alias/tests/test_argument.py index 91940c4d1eb..410dd39a0ad 100644 --- a/src/alias/azext_alias/tests/test_argument.py +++ b/src/alias/azext_alias/tests/test_argument.py @@ -117,7 +117,7 @@ def test_render_template_error(self): 'arg_2': 'test_2' } render_template('{{ arg_1 }} {{ arg_2 }', pos_args_table) - self.assertEqual(str(cm.exception), 'alias: Encounted the following error when injecting positional arguments to ""{{ arg_1 }}" "{{ arg_2 }" - unexpected \'}\'') + self.assertEqual(str(cm.exception), 'alias: Encounted error when injecting positional arguments to ""{{ arg_1 }}" "{{ arg_2 }". Error detail: unexpected \'}\'') def test_check_runtime_errors_no_error(self): pos_args_table = { @@ -133,7 +133,7 @@ def test_check_runtime_errors_has_error(self): 'arg_2': 'test_2' } check_runtime_errors('{{ arg_1.split("_")[2] }} {{ arg_2.split("_")[1] }}', pos_args_table) - self.assertEqual(str(cm.exception), 'alias: Encounted the following error when evaluating "arg_1.split("_")[2]" - list index out of range') + self.assertEqual(str(cm.exception), 'alias: Encounted error when evaluating "arg_1.split("_")[2]". Error detail: list index out of range') if __name__ == '__main__': diff --git a/src/alias/azext_alias/tests/test_custom.py b/src/alias/azext_alias/tests/test_custom.py index 65c399605f2..fbedf70d6d3 100644 --- a/src/alias/azext_alias/tests/test_custom.py +++ b/src/alias/azext_alias/tests/test_custom.py @@ -6,7 +6,7 @@ # pylint: disable=line-too-long,no-self-use,protected-access import unittest -from mock import Mock +from mock import Mock, patch from knack.util import CLIError @@ -22,11 +22,14 @@ class AliasCustomCommandTest(unittest.TestCase): - @classmethod - def setUp(cls): - azext_alias.cached_reserved_commands = TEST_RESERVED_COMMANDS + def setUp(self): + self.patcher = patch('azext_alias.cached_reserved_commands', TEST_RESERVED_COMMANDS) + self.patcher.start() azext_alias.custom._commit_change = Mock() + def tearDown(self): + self.patcher.stop() + def test_create_alias(self): create_alias('ac', 'account') @@ -74,7 +77,7 @@ def test_remove_alias_remove_non_existing_alias(self): mock_alias_table.set('ac', 'command', 'account') azext_alias.custom.get_alias_table = Mock(return_value=mock_alias_table) with self.assertRaises(CLIError) as cm: - remove_alias('dns') + remove_alias(['dns']) self.assertEqual(str(cm.exception), 'alias: "dns" alias not found') diff --git a/src/alias/azext_alias/tests/test_util.py b/src/alias/azext_alias/tests/test_util.py index 0ca9cf51db0..c45db709e76 100644 --- a/src/alias/azext_alias/tests/test_util.py +++ b/src/alias/azext_alias/tests/test_util.py @@ -11,7 +11,6 @@ import unittest import mock -import azext_alias from azext_alias.util import remove_pos_arg_placeholders, build_tab_completion_table, get_config_parser from azext_alias._const import ALIAS_TAB_COMP_TABLE_FILE_NAME from azext_alias.tests._const import TEST_RESERVED_COMMANDS @@ -21,12 +20,15 @@ class TestUtil(unittest.TestCase): def setUp(self): self.mock_config_dir = tempfile.mkdtemp() - self.patcher = mock.patch('azext_alias.util.GLOBAL_ALIAS_TAB_COMP_TABLE_PATH', os.path.join(self.mock_config_dir, ALIAS_TAB_COMP_TABLE_FILE_NAME)) - self.patcher.start() - azext_alias.cached_reserved_commands = TEST_RESERVED_COMMANDS + self.patchers = [] + self.patchers.append(mock.patch('azext_alias.util.GLOBAL_ALIAS_TAB_COMP_TABLE_PATH', os.path.join(self.mock_config_dir, ALIAS_TAB_COMP_TABLE_FILE_NAME))) + self.patchers.append(mock.patch('azext_alias.cached_reserved_commands', TEST_RESERVED_COMMANDS)) + for patcher in self.patchers: + patcher.start() def tearDown(self): - self.patcher.stop() + for patcher in self.patchers: + patcher.stop() shutil.rmtree(self.mock_config_dir) def test_remove_pos_arg_placeholders(self): diff --git a/src/alias/azext_alias/tests/test_validators.py b/src/alias/azext_alias/tests/test_validators.py index f13a2bd9ee9..805c38decb0 100644 --- a/src/alias/azext_alias/tests/test_validators.py +++ b/src/alias/azext_alias/tests/test_validators.py @@ -3,58 +3,151 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long,no-self-use,too-many-public-methods + +import os +import sys +import tempfile import unittest +from mock import patch from knack.util import CLIError -from azext_alias._validators import process_alias_create_namespace +from azext_alias._validators import process_alias_create_namespace, process_alias_import_namespace +from azext_alias.tests._const import TEST_RESERVED_COMMANDS class TestValidators(unittest.TestCase): + def setUp(self): + self.patcher = patch('azext_alias.cached_reserved_commands', TEST_RESERVED_COMMANDS) + self.patcher.start() + + def tearDown(self): + self.patcher.stop() + def test_process_alias_create_namespace_non_existing_command(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('test', 'non existing command')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('test', 'non existing command')) + self.assertEqual(str(cm.exception), 'alias: Invalid Azure CLI command "non existing command"') def test_process_alias_create_namespace_empty_alias_name(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('', 'account')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('', 'account')) + self.assertEqual(str(cm.exception), 'alias: Empty alias name or command is invalid') def test_process_alias_create_namespace_empty_alias_command(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('ac', '')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('ac', '')) + self.assertEqual(str(cm.exception), 'alias: Empty alias name or command is invalid') def test_process_alias_create_namespace_non_existing_commands_with_pos_arg(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('test {{ arg }}', 'account list {{ arg }}')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('test {{ arg }}', 'account list {{ arg }}')) + self.assertEqual(str(cm.exception), 'alias: Invalid Azure CLI command "account list {{ arg }}"') def test_process_alias_create_namespace_inconsistent_pos_arg_name(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('test {{ arg }}', 'account {{ ar }}')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('test {{ arg }}', 'account {{ ar }}')) + if sys.version_info.major == 2: + self.assertTrue(str(cm.exception) in ['alias: Positional arguments set([\'ar\', \'arg\']) are not in both alias name and alias command', 'alias: Positional arguments set([\'arg\', \'ar\']) are not in both alias name and alias command']) + else: + self.assertTrue(str(cm.exception) in ['alias: Positional arguments {\'ar\', \'arg\'} are not in both alias name and alias command', 'alias: Positional arguments {\'arg\', \'ar\'} are not in both alias name and alias command']) def test_process_alias_create_namespace_pos_arg_only(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('test {{ arg }}', '{{ arg }}')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('test {{ arg }}', '{{ arg }}')) + self.assertEqual(str(cm.exception), 'alias: Invalid Azure CLI command "{{ arg }}"') def test_process_alias_create_namespace_inconsistent_number_pos_arg(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('test {{ arg_1 }} {{ arg_2 }}', 'account {{ arg_2 }}')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('test {{ arg_1 }} {{ arg_2 }}', 'account {{ arg_2 }}')) + if sys.version_info.major == 2: + self.assertEqual(str(cm.exception), 'alias: Positional argument set([\'arg_1\']) is not in both alias name and alias command') + else: + self.assertEqual(str(cm.exception), 'alias: Positional argument {\'arg_1\'} is not in both alias name and alias command') def test_process_alias_create_namespace_lvl_error(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('network', 'account list')) + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('network', 'account list')) + self.assertEqual(str(cm.exception), 'alias: Invalid Azure CLI command "account list"') def test_process_alias_create_namespace_lvl_error_with_pos_arg(self): - with self.assertRaises(CLIError): - process_alias_create_namespace(MockNamespace('account {{ test }}', 'dns {{ test }}')) - - -class MockNamespace(object): # pylint: disable=too-few-public-methods + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('account {{ test }}', 'dns {{ test }}')) + self.assertEqual(str(cm.exception), 'alias: "account {{ test }}" is a reserved command and cannot be used to represent "dns {{ test }}"') + + def test_process_alias_create_namespace_pos_arg_1(self): + process_alias_create_namespace(MockAliasCreateNamespace('test', 'group delete resourceGroupName')) + + def test_process_alias_create_namespace_pos_arg_2(self): + process_alias_create_namespace(MockAliasCreateNamespace('test', 'delete resourceGroupName')) + + def test_process_alias_create_namespace_pos_arg_3(self): + process_alias_create_namespace(MockAliasCreateNamespace('test', 'group delete resourceGroupName -p param')) + + def test_process_alias_create_namespace_pos_arg_4(self): + with self.assertRaises(CLIError) as cm: + process_alias_create_namespace(MockAliasCreateNamespace('test', 'group resourceGroupName')) + self.assertEqual(str(cm.exception), 'alias: Invalid Azure CLI command "group resourceGroupName"') + + def test_process_alias_create_namespace_pos_arg_5(self): + process_alias_create_namespace(MockAliasCreateNamespace('test', 'group delete -p param resourceGroupName')) + + def test_process_alias_import_namespace(self): + process_alias_import_namespace(MockAliasImportNamespace('https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/alias')) + + def test_process_alias_import_namespace_invalid_url_python_2(self): + with self.assertRaises(CLIError) as cm: + process_alias_import_namespace(MockAliasImportNamespace('https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/alia')) + if sys.version_info.major == 2: + self.assertEqual(str(cm.exception), 'alias: Encounted error when retrieving alias file from https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/alia. Error detail: 404: Not Found') + else: + self.assertEqual(str(cm.exception), 'alias: Encounted error when retrieving alias file from https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/alia. Error detail: HTTP Error 404: Not Found') + + def test_process_alias_import_namespace_invalid_content_from_url(self): + with self.assertRaises(CLIError) as cm: + process_alias_import_namespace(MockAliasImportNamespace('https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/invalid_alias')) + if sys.version_info.major == 2: + self.assertEqual(str(cm.exception), 'alias: Please ensure you have a valid alias configuration file. Error detail: File contains no alias headers.file: https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/invalid_alias, line: 1\'[c\'') + else: + self.assertEqual(str(cm.exception), 'alias: Please ensure you have a valid alias configuration file. Error detail: File contains no alias headers.file: \'https://raw.githubusercontent.com/chewong/azure-cli-alias-extension/test/azext_alias/tests/invalid_alias\', line: 1\'[c\'') + + def test_process_alias_import_namespace_file(self): + _, mock_alias_config_file = tempfile.mkstemp() + process_alias_import_namespace(MockAliasImportNamespace(mock_alias_config_file)) + os.remove(mock_alias_config_file) + + def test_process_alias_import_namespace_invalid_content_in_file(self): + _, mock_alias_config_file = tempfile.mkstemp() + with open(mock_alias_config_file, 'w') as f: + f.write('invalid alias config format') + with self.assertRaises(CLIError) as cm: + process_alias_import_namespace(MockAliasImportNamespace(mock_alias_config_file)) + if sys.version_info.major == 2: + self.assertEqual(str(cm.exception), 'alias: Please ensure you have a valid alias configuration file. Error detail: File contains no alias headers.file: {}, line: 1\'invalid alias config format\''.format(mock_alias_config_file)) + else: + self.assertEqual(str(cm.exception), 'alias: Please ensure you have a valid alias configuration file. Error detail: File contains no alias headers.file: \'{}\', line: 1\'invalid alias config format\''.format(mock_alias_config_file)) + os.remove(mock_alias_config_file) + + def test_process_alias_import_namespace_dir(self): + with self.assertRaises(CLIError) as cm: + process_alias_import_namespace(MockAliasImportNamespace(os.getcwd())) + self.assertEqual(str(cm.exception), 'alias: {} is a directory'.format(os.getcwd())) + + +class MockAliasCreateNamespace(object): # pylint: disable=too-few-public-methods def __init__(self, alias_name, alias_command): self.alias_name = alias_name self.alias_command = alias_command +class MockAliasImportNamespace(object): # pylint: disable=too-few-public-methods + + def __init__(self, alias_source): + self.alias_source = alias_source + + if __name__ == '__main__': unittest.main() diff --git a/src/alias/azext_alias/util.py b/src/alias/azext_alias/util.py index d827d613bce..379cd6692eb 100644 --- a/src/alias/azext_alias/util.py +++ b/src/alias/azext_alias/util.py @@ -3,15 +3,21 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# pylint: disable=wrong-import-order,import-error,relative-import + import re import sys import json import shlex from collections import defaultdict from six.moves import configparser +from six.moves.urllib.parse import urlparse +from six.moves.urllib.request import urlretrieve + +from knack.util import CLIError import azext_alias -from azext_alias._const import COLLISION_CHECK_LEVEL_DEPTH, GLOBAL_ALIAS_TAB_COMP_TABLE_PATH +from azext_alias._const import COLLISION_CHECK_LEVEL_DEPTH, GLOBAL_ALIAS_TAB_COMP_TABLE_PATH, ALIAS_FILE_URL_ERROR def get_config_parser(): @@ -38,14 +44,25 @@ def get_alias_table(): return get_config_parser() -def is_alias_create_command(args): +def is_alias_command(subcommands, args): """ - Check if the user is invoking 'az alias create'. + Check if the user is invoking one of the comments in 'subcommands' in the from az alias . + + Args: + subcommands: The list of subcommands to check through. + args: The CLI arguments to process. Returns: - True if the user is invoking 'az alias create'. + True if the user is invoking 'az alias {command}'. """ - return args and args[:2] == ['alias', 'create'] + if not args: + return False + + for subcommand in subcommands: + if args[:2] == ['alias', subcommand]: + return True + + return False def cache_reserved_commands(load_cmd_tbl_func): @@ -54,6 +71,9 @@ def cache_reserved_commands(load_cmd_tbl_func): for alias and command validation when the user invokes alias create). This cache saves the entire command table globally so custom.py can have access to it. Alter this cache through cache_reserved_commands(load_cmd_tbl_func) in util.py. + + Args: + load_cmd_tbl_func: The function to load the entire command table. """ if not azext_alias.cached_reserved_commands: azext_alias.cached_reserved_commands = list(load_cmd_tbl_func([]).keys()) @@ -65,6 +85,9 @@ def remove_pos_arg_placeholders(alias_command): Args: alias_command: The alias command to remove from. + + Returns: + The alias command string without positional argument placeholder. """ # Boundary index is the index at which named argument or positional argument starts split_command = shlex.split(alias_command) @@ -130,3 +153,73 @@ def build_tab_completion_table(alias_table): f.write(json.dumps(tab_completion_table)) return tab_completion_table + + +def is_url(s): + """ + Check if the argument is an URL. + + Returns: + True if the argument is an URL. + """ + return urlparse(s).scheme in ('http', 'https') + + +def reduce_alias_table(alias_table): + """ + Reduce the alias table to a tuple that contains the alias and the command that the alias points to. + + Args: + The alias table to be reduced. + + Yields + A tuple that contains the alias and the command that the alias points to. + """ + for alias in alias_table.sections(): + if alias_table.has_option(alias, 'command'): + yield (alias, alias_table.get(alias, 'command')) + + +def retrieve_file_from_url(url): + """ + Retrieve a file from an URL + + Args: + url: The URL to retrieve the file from. + + Returns: + The absolute path of the downloaded file. + """ + try: + alias_source, _ = urlretrieve(url) + # Check for HTTPError in Python 2.x + with open(alias_source, 'r') as f: + content = f.read() + if content[:3].isdigit(): + raise CLIError(ALIAS_FILE_URL_ERROR.format(url, content.strip())) + except Exception as exception: + if isinstance(exception, CLIError): + raise + + # Python 3.x + raise CLIError(ALIAS_FILE_URL_ERROR.format(url, exception)) + + return alias_source + + +def filter_alias_create_namespace(namespace): + """ + Filter alias name and alias command inside alias create namespace to appropriate strings. + + Args + namespace: The alias create namespace. + + Returns: + Filtered namespace where excessive whitespaces are removed in strings. + """ + def filter_string(s): + return ' '.join(s.strip().split()) + + namespace.alias_name = filter_string(namespace.alias_name) + namespace.alias_command = filter_string(namespace.alias_command) + return namespace diff --git a/src/alias/azext_alias/version.py b/src/alias/azext_alias/version.py index 1f8c0a35913..0e6de423b13 100644 --- a/src/alias/azext_alias/version.py +++ b/src/alias/azext_alias/version.py @@ -3,4 +3,4 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -VERSION = '0.4.0' +VERSION = '0.5.0' diff --git a/src/index.json b/src/index.json index 2d0de045965..a8e08d2273a 100644 --- a/src/index.json +++ b/src/index.json @@ -461,9 +461,9 @@ ], "alias": [ { - "filename": "alias-0.4.0-py2.py3-none-any.whl", - "sha256Digest": "fdc818e1b814c7f687f4cda4718e6a1c16eb827eac72b22afc5368dce9cdd358", - "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/alias-0.4.0-py2.py3-none-any.whl", + "filename": "alias-0.5.0-py2.py3-none-any.whl", + "sha256Digest": "f50723edb97a4f67535e0832d843abebf8272152efc4ec8201a48307999aa59c", + "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/alias-0.5.0-py2.py3-none-any.whl", "metadata": { "azext.isPreview": true, "azext.minCliCoreVersion": "2.0.31.dev0", @@ -510,7 +510,7 @@ } ], "summary": "Support for command aliases", - "version": "0.4.0" + "version": "0.5.0" } } ],