diff options
-rw-r--r-- | cmd2/cmd2.py | 84 | ||||
-rw-r--r-- | cmd2/parsing.py | 37 |
2 files changed, 78 insertions, 43 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e35d216e..1c020323 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -47,7 +47,7 @@ from . import utils from . import plugin from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer -from .parsing import StatementParser, Statement, Macro, normal_arg_pattern, escaped_arg_pattern, digit_pattern +from .parsing import StatementParser, Statement, Macro, MacroArgInfo, macro_normal_arg_pattern, macro_escaped_arg_pattern, digit_pattern # Set up readline from .rl_utils import rl_type, RlType @@ -1513,7 +1513,7 @@ class Cmd(cmd.Cmd): else: # Complete token against anything a user can run self.completion_matches = self.basic_complete(text, line, begidx, endidx, - self._get_all_runnable_names()) + self._get_completable_runnables()) # Handle single result if len(self.completion_matches) == 1: @@ -1540,8 +1540,8 @@ class Cmd(cmd.Cmd): except IndexError: return None - def _get_all_runnable_names(self) -> List[str]: - """Return a list of all commands, aliases, and macros""" + def _get_completable_runnables(self) -> List[str]: + """Return a list of all commands, aliases, and macros that would show up in tab completion""" visible_commands = set(self.get_visible_commands()) alias_names = set(self._get_alias_names()) macro_names = set(self._get_macro_names()) @@ -2247,17 +2247,20 @@ class Cmd(cmd.Cmd): self.perror(errmsg, traceback_war=False) return - stripped_command = args.command.strip() - if not stripped_command: - errmsg = "An alias cannot resolve to an empty string" + if not args.command.strip(): + errmsg = "Aliases cannot resolve to an empty command" self.perror(errmsg, traceback_war=False) return - # Build the alias command string - command = stripped_command + ' ' + ' '.join(utils.quote_string_if_needed(args.command_args)) + # Build the alias value string + arg_str = '' + for cur_arg in args.command_args: + arg_str += ' ' + utils.quote_string_if_needed(cur_arg) + + value = utils.quote_string_if_needed(args.command) + arg_str # Set the alias - self.aliases[args.name] = command + self.aliases[args.name] = value self.poutput("Alias {!r} created".format(args.name)) def alias_delete(self, args: argparse.Namespace): @@ -2315,9 +2318,9 @@ class Cmd(cmd.Cmd): description=alias_create_description, epilog=alias_create_epilog) setattr(alias_create_parser.add_argument('name', type=str, help='Name of this alias'), - ACTION_ARG_CHOICES, _get_all_runnable_names) + ACTION_ARG_CHOICES, _get_completable_runnables) setattr(alias_create_parser.add_argument('command', type=str, help='what the alias resolves to'), - ACTION_ARG_CHOICES, _get_all_runnable_names) + ACTION_ARG_CHOICES, _get_completable_runnables) setattr(alias_create_parser.add_argument('command_args', type=str, nargs=argparse.REMAINDER, help='arguments being passed to command'), ACTION_ARG_CHOICES, ('path_complete',)) @@ -2378,18 +2381,21 @@ class Cmd(cmd.Cmd): self.perror(errmsg, traceback_war=False) return - stripped_command = args.command.strip() - if not stripped_command: - errmsg = "An macro cannot resolve to an empty string" + if not args.command.strip(): + errmsg = "Macros cannot resolve to an empty command" self.perror(errmsg, traceback_war=False) return # Build the macro value string - value = stripped_command + ' ' + ' '.join(utils.quote_string_if_needed(args.command_args)) + arg_str = '' + for cur_arg in args.command_args: + arg_str += ' ' + utils.quote_string_if_needed(cur_arg) + + value = utils.quote_string_if_needed(args.command) + arg_str # Find all normal arguments arg_info_list = [] - normal_matches = re.finditer(normal_arg_pattern, value) + normal_matches = re.finditer(macro_normal_arg_pattern, value) max_arg_num = 0 num_set = set() @@ -2403,9 +2409,9 @@ class Cmd(cmd.Cmd): if cur_num > max_arg_num: max_arg_num = cur_num - arg_info_list.append(Macro.ArgInfo(start_index=cur_match.start(), - number=cur_num, - is_escaped=False)) + arg_info_list.append(MacroArgInfo(start_index=cur_match.start(), + number=cur_num, + is_escaped=False)) except StopIteration: break @@ -2417,7 +2423,7 @@ class Cmd(cmd.Cmd): return # Find all escaped arguments - escaped_matches = re.finditer(escaped_arg_pattern, value) + escaped_matches = re.finditer(macro_escaped_arg_pattern, value) while True: try: @@ -2426,9 +2432,9 @@ class Cmd(cmd.Cmd): # Get the number between the braces cur_num = int(re.findall(digit_pattern, cur_match.group())[0]) - arg_info_list.append(Macro.ArgInfo(start_index=cur_match.start(), - number=cur_num, - is_escaped=True)) + arg_info_list.append(MacroArgInfo(start_index=cur_match.start(), + number=cur_num, + is_escaped=True)) except StopIteration: break @@ -2437,6 +2443,24 @@ class Cmd(cmd.Cmd): min_arg_count=max_arg_num, arg_info_list=arg_info_list) self.poutput("Macro {!r} created".format(args.name)) + def macro_delete(self, args: argparse.Namespace): + """ Deletes macros """ + if args.all: + self.macros.clear() + self.poutput("All macros deleted") + elif not args.name: + self.do_help(['macro', 'delete']) + else: + # Get rid of duplicates + macros_to_delete = utils.remove_duplicates(args.name) + + for cur_arg in macros_to_delete: + if cur_arg in self.macros: + del self.macros[cur_arg] + self.poutput("Macro {!r} cleared".format(cur_arg)) + else: + self.perror("Macro {!r} does not exist".format(cur_arg), traceback_war=False) + def macro_list(self, args: argparse.Namespace): """ Lists some or all macros """ if args.name: @@ -2483,12 +2507,22 @@ class Cmd(cmd.Cmd): setattr(macro_create_parser.add_argument('name', type=str, help='Name of this macro'), ACTION_ARG_CHOICES, _get_macro_names) setattr(macro_create_parser.add_argument('command', type=str, help='what the macro resolves to'), - ACTION_ARG_CHOICES, _get_all_runnable_names) + ACTION_ARG_CHOICES, _get_completable_runnables) setattr(macro_create_parser.add_argument('command_args', type=str, nargs=argparse.REMAINDER, help='arguments being passed to command'), ACTION_ARG_CHOICES, ('path_complete',)) macro_create_parser.set_defaults(func=macro_create) + # macro -> delete + macro_delete_help = "delete macros" + macro_delete_description = "Delete specified macros or all macros if --all is used" + macro_delete_parser = macro_subparsers.add_parser('delete', help=macro_delete_help, + description=macro_delete_description) + setattr(macro_delete_parser.add_argument('name', type=str, nargs='*', help='macro to delete'), + ACTION_ARG_CHOICES, _get_macro_names) + macro_delete_parser.add_argument('-a', '--all', action='store_true', help="all macros will be deleted") + macro_delete_parser.set_defaults(func=macro_delete) + # macro -> list macro_list_help = "list macros" macro_list_description = "List specified macros in a reusable form that can be saved to\n" diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 4d17d07e..7a2af64b 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -14,35 +14,36 @@ from . import utils # Pattern used to find normal argument # Match strings like: {5}, {{{{{4}, {2}}}}} -normal_arg_pattern = re.compile(r'(?<!\{)\{\d+\}|\{\d+\}(?!\})') +macro_normal_arg_pattern = re.compile(r'(?<!\{)\{\d+\}|\{\d+\}(?!\})') # Pattern used to find escaped arguments (2 or more braces on each side of digit) # Match strings like: {{5}}, {{{{{4}}, {{2}}}}}, {{{4}}} -escaped_arg_pattern = re.compile(r'\{{2}\d+\}{2}') +macro_escaped_arg_pattern = re.compile(r'\{{2}\d+\}{2}') # Finds a string of digits digit_pattern = re.compile(r'\d+') @attr.s(frozen=True) -class Macro: - """Defines a cmd2 macro""" +class MacroArgInfo: + """ + Information used to replace or unescape arguments in a macro value when the macro is resolved + Normal argument syntax : {5} + Escaped argument syntax: {{5}} + """ + # The starting index of this argument in the macro value + start_index = attr.ib(validator=attr.validators.instance_of(int), type=int) - @attr.s(frozen=True) - class ArgInfo: - """ - Information used to replace or unescape arguments in a macro value when the macro is resolved - Normal argument syntax : {5} - Escaped argument syntax: {{5}} - """ - # The starting index of this argument in the macro value - start_index = attr.ib(validator=attr.validators.instance_of(int), type=int) + # The number that appears between the braces + number = attr.ib(validator=attr.validators.instance_of(int), type=int) + + # Tells if this argument is escaped and therefore needs to be unescaped + is_escaped = attr.ib(validator=attr.validators.instance_of(bool), type=bool) - # The number that appears between the braces - number = attr.ib(validator=attr.validators.instance_of(int), type=int) - # Tells if this argument is escaped and therefore needs to be unescaped - is_escaped = attr.ib(validator=attr.validators.instance_of(bool), type=bool) +@attr.s(frozen=True) +class Macro: + """Defines a cmd2 macro""" # Name of the macro name = attr.ib(validator=attr.validators.instance_of(str), type=str) @@ -54,7 +55,7 @@ class Macro: min_arg_count = attr.ib(validator=attr.validators.instance_of(int), type=int) # Used to fill in argument placeholders in the macro - arg_info_list = attr.ib(factory=list, validator=attr.validators.instance_of(list), type=List[ArgInfo]) + arg_info_list = attr.ib(factory=list, validator=attr.validators.instance_of(list), type=List[MacroArgInfo]) @attr.s(frozen=True) |