Skip to content
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.5.0 #131

Merged
merged 6 commits into from
Apr 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/alias/azext_alias/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@
# 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
from azure.cli.command_modules.interactive.events import (
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,
enable_aliases_autocomplete,
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
Expand All @@ -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

Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/alias/azext_alias/_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@
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 "{}"'
EMPTY_ALIAS_ERROR = 'alias: Empty alias name or command is invalid'
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.'
20 changes: 19 additions & 1 deletion src/alias/azext_alias/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
"""
123 changes: 120 additions & 3 deletions src/alias/azext_alias/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import re
import shlex

from knack.util import CLIError

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

Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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)))
7 changes: 4 additions & 3 deletions src/alias/azext_alias/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down
Loading