From 0bdeb6217a0657b9e59eb5a44d43184f5e95cea7 Mon Sep 17 00:00:00 2001
From: Kevin Van Brunt <kmvanbrunt@gmail.com>
Date: Thu, 24 Oct 2024 23:28:03 -0400
Subject: [PATCH] Removed macros.

---
 CHANGELOG.md                                  |   4 +
 README.md                                     |   4 +-
 cmd2/cmd2.py                                  | 394 +-----------------
 cmd2/parsing.py                               |  58 +--
 docs/api/cmd.rst                              |   2 +-
 docs/api/parsing.rst                          |   2 +-
 docs/examples/first_app.rst                   |   6 +-
 docs/features/builtin_commands.rst            |  12 +-
 docs/features/commands.rst                    |   2 +-
 docs/features/help.rst                        |  88 ++--
 docs/features/history.rst                     |  14 +-
 docs/features/index.rst                       |   2 +-
 docs/features/initialization.rst              |   1 -
 docs/features/os.rst                          |   6 +-
 ...iases_macros.rst => shortcuts_aliases.rst} |  48 +--
 docs/migrating/incompatibilities.rst          |   2 +-
 docs/migrating/why.rst                        |   4 +-
 examples/help_categories.py                   |   3 +
 tests/conftest.py                             |   3 +-
 tests/test_cmd2.py                            | 255 +-----------
 tests/test_completion.py                      |  21 -
 tests/test_parsing.py                         | 108 -----
 tests_isolated/test_commandset/conftest.py    |   3 +-
 .../test_commandset/test_commandset.py        |   8 +-
 24 files changed, 107 insertions(+), 943 deletions(-)
 rename docs/features/{shortcuts_aliases_macros.rst => shortcuts_aliases.rst} (57%)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 417d9b427..291397a47 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.0.0 (TBD)
+* Breaking Change
+  * Removed macros
+
 ## 2.5.0 (October 23, 2024)
 * Breaking Change
   * `cmd2` 2.5 supports Python 3.8+ (removed support for Python 3.6 and 3.7)
diff --git a/README.md b/README.md
index ac65e91c0..5ccd594a4 100755
--- a/README.md
+++ b/README.md
@@ -62,9 +62,9 @@ Deep extensive tab completion and help text generation based on the argparse lib
 
 <a href="https://imgflip.com/i/66t0y0"><img src="https://i.imgflip.com/66t0y0.jpg" title="made at imgflip.com" width="70%" height="70%"/></a>
 
-cmd2 creates the second pillar of 'ease of transition to automation' through alias/macro creation, command line argument parsing and execution of cmd2 scripting.
+cmd2 creates the second pillar of 'ease of transition to automation' through alias creation, command line argument parsing and execution of cmd2 scripting.
 
-- Flexible alias and macro creation for quick abstraction of commands.
+- Flexible alias creation for quick abstraction of commands.
 - Text file scripting of your application with `run_script` (`@`) and `_relative_run_script` (`@@`)
 - Powerful and flexible built-in Python scripting of your application using the `run_pyscript` command
 - Transcripts for use with built-in regression can be automatically generated from `history -t` or `run_script -t`
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index dbd993495..b53ae7a20 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -37,7 +37,6 @@
 import inspect
 import os
 import pydoc
-import re
 import sys
 import tempfile
 import threading
@@ -122,8 +121,6 @@
     single_line_format,
 )
 from .parsing import (
-    Macro,
-    MacroArg,
     Statement,
     StatementParser,
     shlex_split,
@@ -363,9 +360,6 @@ def __init__(
         # Commands to exclude from the history command
         self.exclude_from_history = ['eof', 'history']
 
-        # Dictionary of macro names and their values
-        self.macros: Dict[str, Macro] = dict()
-
         # Keeps track of typed command history in the Python shell
         self._py_history: List[str] = []
 
@@ -411,7 +405,7 @@ def __init__(
         self.help_error = "No help on {}"
 
         # The error that prints when a non-existent command is run
-        self.default_error = "{} is not a recognized command, alias, or macro."
+        self.default_error = "{} is not a recognized command or alias."
 
         # If non-empty, this string will be displayed if a broken pipe error occurs
         self.broken_pipe_warning = ''
@@ -482,7 +476,7 @@ def __init__(
         # If natural sorting is preferred, then set this to NATURAL_SORT_KEY.
         # cmd2 uses this key for sorting:
         #     command and category names
-        #     alias, macro, settable, and shortcut names
+        #     alias, settable, and shortcut names
         #     tab completion results when self.matches_sorted is False
         self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY
 
@@ -749,11 +743,6 @@ def _install_command_function(self, command: str, command_wrapper: Callable[...,
             self.pwarning(f"Deleting alias '{command}' because it shares its name with a new command")
             del self.aliases[command]
 
-        # Check if command shares a name with a macro
-        if command in self.macros:
-            self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
-            del self.macros[command]
-
         self._register_command_parser(command, command_wrapper)
 
         setattr(self, cmd_func_name, command_wrapper)
@@ -2077,12 +2066,8 @@ def _perform_completion(
 
         # Determine the completer function to use for the command's argument
         if custom_settings is None:
-            # Check if a macro was entered
-            if command in self.macros:
-                completer_func = self.path_complete
-
             # Check if a command was entered
-            elif command in self.get_all_commands():
+            if command in self.get_all_commands():
                 # Get the completer function for this command
                 func_attr = getattr(self, constants.COMPLETER_FUNC_PREFIX + command, None)
 
@@ -2108,7 +2093,7 @@ def _perform_completion(
                     else:
                         completer_func = self.completedefault  # type: ignore[assignment]
 
-            # Not a recognized macro or command
+            # Not a recognized command
             else:
                 # Check if this command should be run as a shell command
                 if self.default_to_shell and command in utils.get_exes_in_path(command):
@@ -2271,8 +2256,8 @@ def complete(  # type: ignore[override]
                         parser.add_argument(
                             'command',
                             metavar="COMMAND",
-                            help="command, alias, or macro name",
-                            choices=self._get_commands_aliases_and_macros_for_completion(),
+                            help="command or alias name",
+                            choices=self._get_commands_and_aliases_for_completion(),
                         )
                         custom_settings = utils.CustomCompletionSettings(parser)
 
@@ -2360,19 +2345,6 @@ def _get_alias_completion_items(self) -> List[CompletionItem]:
 
         return results
 
-    # Table displayed when tab completing macros
-    _macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
-
-    def _get_macro_completion_items(self) -> List[CompletionItem]:
-        """Return list of macro names and values as CompletionItems"""
-        results: List[CompletionItem] = []
-
-        for cur_key in self.macros:
-            row_data = [self.macros[cur_key].value]
-            results.append(CompletionItem(cur_key, self._macro_completion_table.generate_data_row(row_data)))
-
-        return results
-
     # Table displayed when tab completing Settables
     _settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None)
 
@@ -2386,12 +2358,11 @@ def _get_settable_completion_items(self) -> List[CompletionItem]:
 
         return results
 
-    def _get_commands_aliases_and_macros_for_completion(self) -> List[str]:
-        """Return a list of visible commands, aliases, and macros for tab completion"""
+    def _get_commands_and_aliases_for_completion(self) -> List[str]:
+        """Return a list of visible commands and aliases for tab completion"""
         visible_commands = set(self.get_visible_commands())
         alias_names = set(self.aliases)
-        macro_names = set(self.macros)
-        return list(visible_commands | alias_names | macro_names)
+        return list(visible_commands | alias_names)
 
     def get_help_topics(self) -> List[str]:
         """Return a list of help topics"""
@@ -2540,7 +2511,7 @@ def onecmd_plus_hooks(
 
         try:
             # Convert the line into a Statement
-            statement = self._input_line_to_statement(line, orig_rl_history_length=orig_rl_history_length)
+            statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
 
             # call the postparsing hooks
             postparsing_data = plugin.PostparsingData(False, statement)
@@ -2785,101 +2756,6 @@ def combine_rl_history(statement: Statement) -> None:
 
         return statement
 
-    def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
-        """
-        Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
-
-        :param line: the line being parsed
-        :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
-                                       This is used to assist in combining multiline readline history entries and is only
-                                       populated by cmd2. Defaults to None.
-        :return: parsed command line as a Statement
-        :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
-        :raises: EmptyStatement when the resulting Statement is blank
-        """
-        used_macros = []
-        orig_line = None
-
-        # Continue until all macros are resolved
-        while True:
-            # Make sure all input has been read and convert it to a Statement
-            statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
-
-            # If this is the first loop iteration, save the original line and stop
-            # combining multiline history entries in the remaining iterations.
-            if orig_line is None:
-                orig_line = statement.raw
-                orig_rl_history_length = None
-
-            # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
-            if statement.command in self.macros.keys() and statement.command not in used_macros:
-                used_macros.append(statement.command)
-                resolve_result = self._resolve_macro(statement)
-                if resolve_result is None:
-                    raise EmptyStatement
-                line = resolve_result
-            else:
-                break
-
-        # This will be true when a macro was used
-        if orig_line != statement.raw:
-            # Build a Statement that contains the resolved macro line
-            # but the originally typed line for its raw member.
-            statement = Statement(
-                statement.args,
-                raw=orig_line,
-                command=statement.command,
-                arg_list=statement.arg_list,
-                multiline_command=statement.multiline_command,
-                terminator=statement.terminator,
-                suffix=statement.suffix,
-                pipe_to=statement.pipe_to,
-                output=statement.output,
-                output_to=statement.output_to,
-            )
-        return statement
-
-    def _resolve_macro(self, statement: Statement) -> Optional[str]:
-        """
-        Resolve a macro and return the resulting string
-
-        :param statement: the parsed statement from the command line
-        :return: the resolved macro or None on error
-        """
-        if statement.command not in self.macros.keys():
-            raise KeyError(f"{statement.command} is not a macro")
-
-        macro = self.macros[statement.command]
-
-        # Make sure enough arguments were passed in
-        if len(statement.arg_list) < macro.minimum_arg_count:
-            plural = '' if macro.minimum_arg_count == 1 else 's'
-            self.perror(f"The macro '{statement.command}' expects at least {macro.minimum_arg_count} argument{plural}")
-            return None
-
-        # Resolve the arguments in reverse and read their values from statement.argv since those
-        # are unquoted. Macro args should have been quoted when the macro was created.
-        resolved = macro.value
-        reverse_arg_list = sorted(macro.arg_list, key=lambda ma: ma.start_index, reverse=True)
-
-        for macro_arg in reverse_arg_list:
-            if macro_arg.is_escaped:
-                to_replace = '{{' + macro_arg.number_str + '}}'
-                replacement = '{' + macro_arg.number_str + '}'
-            else:
-                to_replace = '{' + macro_arg.number_str + '}'
-                replacement = statement.argv[int(macro_arg.number_str)]
-
-            parts = resolved.rsplit(to_replace, maxsplit=1)
-            resolved = parts[0] + replacement + parts[1]
-
-        # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
-        for stmt_arg in statement.arg_list[macro.minimum_arg_count :]:
-            resolved += ' ' + stmt_arg
-
-        # Restore any terminator, suffix, redirection, etc.
-        return resolved + statement.post_command
-
     def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
         """Set up a command's output redirection for >, >>, and |.
 
@@ -3063,7 +2939,7 @@ def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = Tru
         """
         # For backwards compatibility with cmd, allow a str to be passed in
         if not isinstance(statement, Statement):
-            statement = self._input_line_to_statement(statement)
+            statement = self._complete_statement(statement)
 
         func = self.cmd_func(statement.command)
         if func:
@@ -3395,8 +3271,7 @@ def _cmdloop(self) -> None:
 
     # Top-level parser for alias
     alias_description = "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string."
-    alias_epilog = "See also:\n" "  macro"
-    alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
+    alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description)
     alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
     alias_subparsers.required = True
 
@@ -3430,7 +3305,7 @@ def do_alias(self, args: argparse.Namespace) -> None:
     )
     alias_create_parser.add_argument('name', help='name of this alias')
     alias_create_parser.add_argument(
-        'command', help='what the alias resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
+        'command', help='what the alias resolves to', choices_provider=_get_commands_and_aliases_for_completion
     )
     alias_create_parser.add_argument(
         'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
@@ -3451,10 +3326,6 @@ def _alias_create(self, args: argparse.Namespace) -> None:
             self.perror("Alias cannot have the same name as a command")
             return
 
-        if args.name in self.macros:
-            self.perror("Alias cannot have the same name as a macro")
-            return
-
         # Unquote redirection and terminator tokens
         tokens_to_unquote = constants.REDIRECTION_TOKENS
         tokens_to_unquote.extend(self.statement_parser.terminators)
@@ -3558,243 +3429,6 @@ def _alias_list(self, args: argparse.Namespace) -> None:
         for name in not_found:
             self.perror(f"Alias '{name}' not found")
 
-    #############################################################
-    # Parsers and functions for macro command and subcommands
-    #############################################################
-
-    # Top-level parser for macro
-    macro_description = "Manage macros\n" "\n" "A macro is similar to an alias, but it can contain argument placeholders."
-    macro_epilog = "See also:\n" "  alias"
-    macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
-    macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND')
-    macro_subparsers.required = True
-
-    # Preserve quotes since we are passing strings to other commands
-    @with_argparser(macro_parser, preserve_quotes=True)
-    def do_macro(self, args: argparse.Namespace) -> None:
-        """Manage macros"""
-        # Call handler for whatever subcommand was selected
-        handler = args.cmd2_handler.get()
-        handler(args)
-
-    # macro -> create
-    macro_create_help = "create or overwrite a macro"
-    macro_create_description = "Create or overwrite a macro"
-
-    macro_create_epilog = (
-        "A macro is similar to an alias, but it can contain argument placeholders.\n"
-        "Arguments are expressed when creating a macro using {#} notation where {1}\n"
-        "means the first argument.\n"
-        "\n"
-        "The following creates a macro called my_macro that expects two arguments:\n"
-        "\n"
-        "  macro create my_macro make_dinner --meat {1} --veggie {2}\n"
-        "\n"
-        "When the macro is called, the provided arguments are resolved and the\n"
-        "assembled command is run. For example:\n"
-        "\n"
-        "  my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n"
-        "\n"
-        "Notes:\n"
-        "  To use the literal string {1} in your command, escape it this way: {{1}}.\n"
-        "\n"
-        "  Extra arguments passed to a macro are appended to resolved command.\n"
-        "\n"
-        "  An argument number can be repeated in a macro. In the following example the\n"
-        "  first argument will populate both {1} instances.\n"
-        "\n"
-        "    macro create ft file_taxes -p {1} -q {2} -r {1}\n"
-        "\n"
-        "  To quote an argument in the resolved command, quote it during creation.\n"
-        "\n"
-        "    macro create backup !cp \"{1}\" \"{1}.orig\"\n"
-        "\n"
-        "  If you want to use redirection, pipes, or terminators in the value of the\n"
-        "  macro, then quote them.\n"
-        "\n"
-        "    macro create show_results print_results -type {1} \"|\" less\n"
-        "\n"
-        "  Because macros do not resolve until after hitting Enter, tab completion\n"
-        "  will only complete paths while typing a macro."
-    )
-
-    macro_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
-        description=macro_create_description, epilog=macro_create_epilog
-    )
-    macro_create_parser.add_argument('name', help='name of this macro')
-    macro_create_parser.add_argument(
-        'command', help='what the macro resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
-    )
-    macro_create_parser.add_argument(
-        'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
-    )
-
-    @as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help)
-    def _macro_create(self, args: argparse.Namespace) -> None:
-        """Create or overwrite a macro"""
-        self.last_result = False
-
-        # Validate the macro name
-        valid, errmsg = self.statement_parser.is_valid_command(args.name)
-        if not valid:
-            self.perror(f"Invalid macro name: {errmsg}")
-            return
-
-        if args.name in self.get_all_commands():
-            self.perror("Macro cannot have the same name as a command")
-            return
-
-        if args.name in self.aliases:
-            self.perror("Macro cannot have the same name as an alias")
-            return
-
-        # Unquote redirection and terminator tokens
-        tokens_to_unquote = constants.REDIRECTION_TOKENS
-        tokens_to_unquote.extend(self.statement_parser.terminators)
-        utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
-
-        # Build the macro value string
-        value = args.command
-        if args.command_args:
-            value += ' ' + ' '.join(args.command_args)
-
-        # Find all normal arguments
-        arg_list = []
-        normal_matches = re.finditer(MacroArg.macro_normal_arg_pattern, value)
-        max_arg_num = 0
-        arg_nums = set()
-
-        while True:
-            try:
-                cur_match = normal_matches.__next__()
-
-                # Get the number string between the braces
-                cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0]
-                cur_num = int(cur_num_str)
-                if cur_num < 1:
-                    self.perror("Argument numbers must be greater than 0")
-                    return
-
-                arg_nums.add(cur_num)
-                if cur_num > max_arg_num:
-                    max_arg_num = cur_num
-
-                arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False))
-
-            except StopIteration:
-                break
-
-        # Make sure the argument numbers are continuous
-        if len(arg_nums) != max_arg_num:
-            self.perror(f"Not all numbers between 1 and {max_arg_num} are present in the argument placeholders")
-            return
-
-        # Find all escaped arguments
-        escaped_matches = re.finditer(MacroArg.macro_escaped_arg_pattern, value)
-
-        while True:
-            try:
-                cur_match = escaped_matches.__next__()
-
-                # Get the number string between the braces
-                cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0]
-
-                arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True))
-            except StopIteration:
-                break
-
-        # Set the macro
-        result = "overwritten" if args.name in self.macros else "created"
-        self.poutput(f"Macro '{args.name}' {result}")
-
-        self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
-        self.last_result = True
-
-    # macro -> delete
-    macro_delete_help = "delete macros"
-    macro_delete_description = "Delete specified macros or all macros if --all is used"
-    macro_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_delete_description)
-    macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
-    macro_delete_parser.add_argument(
-        'names',
-        nargs=argparse.ZERO_OR_MORE,
-        help='macro(s) to delete',
-        choices_provider=_get_macro_completion_items,
-        descriptive_header=_macro_completion_table.generate_header(),
-    )
-
-    @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help)
-    def _macro_delete(self, args: argparse.Namespace) -> None:
-        """Delete macros"""
-        self.last_result = True
-
-        if args.all:
-            self.macros.clear()
-            self.poutput("All macros deleted")
-        elif not args.names:
-            self.perror("Either --all or macro name(s) must be specified")
-            self.last_result = False
-        else:
-            for cur_name in utils.remove_duplicates(args.names):
-                if cur_name in self.macros:
-                    del self.macros[cur_name]
-                    self.poutput(f"Macro '{cur_name}' deleted")
-                else:
-                    self.perror(f"Macro '{cur_name}' does not exist")
-
-    # macro -> list
-    macro_list_help = "list macros"
-    macro_list_description = (
-        "List specified macros in a reusable form that can be saved to a startup script\n"
-        "to preserve macros across sessions\n"
-        "\n"
-        "Without arguments, all macros will be listed."
-    )
-
-    macro_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_list_description)
-    macro_list_parser.add_argument(
-        'names',
-        nargs=argparse.ZERO_OR_MORE,
-        help='macro(s) to list',
-        choices_provider=_get_macro_completion_items,
-        descriptive_header=_macro_completion_table.generate_header(),
-    )
-
-    @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
-    def _macro_list(self, args: argparse.Namespace) -> None:
-        """List some or all macros as 'macro create' commands"""
-        self.last_result = {}  # Dict[macro_name, macro_value]
-
-        tokens_to_quote = constants.REDIRECTION_TOKENS
-        tokens_to_quote.extend(self.statement_parser.terminators)
-
-        if args.names:
-            to_list = utils.remove_duplicates(args.names)
-        else:
-            to_list = sorted(self.macros, key=self.default_sort_key)
-
-        not_found: List[str] = []
-        for name in to_list:
-            if name not in self.macros:
-                not_found.append(name)
-                continue
-
-            # Quote redirection and terminator tokens for the 'macro create' command
-            tokens = shlex_split(self.macros[name].value)
-            command = tokens[0]
-            command_args = tokens[1:]
-            utils.quote_specific_tokens(command_args, tokens_to_quote)
-
-            val = command
-            if command_args:
-                val += ' ' + ' '.join(command_args)
-
-            self.poutput(f"macro create {name} {val}")
-            self.last_result[name] = val
-
-        for name in not_found:
-            self.perror(f"Macro '{name}' not found")
-
     def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
         """Completes the command argument of help"""
 
@@ -4712,7 +4346,7 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]:  # pragma: no cover
         '-x',
         '--expanded',
         action='store_true',
-        help='output fully parsed commands with any aliases and\n' 'macros expanded, instead of typed commands',
+        help='output fully parsed commands with aliases and shortcuts expanded',
     )
     history_format_group.add_argument(
         '-v',
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index e84f7c4fc..7ef1cc3de 100755
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -38,56 +38,6 @@ def shlex_split(str_to_split: str) -> List[str]:
     return shlex.split(str_to_split, comments=False, posix=False)
 
 
-@dataclass(frozen=True)
-class MacroArg:
-    """
-    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: int
-
-    # The number string that appears between the braces
-    # This is a string instead of an int because we support unicode digits and must be able
-    # to reproduce this string later
-    number_str: str
-
-    # Tells if this argument is escaped and therefore needs to be unescaped
-    is_escaped: bool
-
-    # Pattern used to find normal argument
-    # Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
-    # Match strings like: {5}, {{{{{4}, {2}}}}}
-    macro_normal_arg_pattern = re.compile(r'(?<!{){\d+}|{\d+}(?!})')
-
-    # Pattern used to find escaped arguments
-    # Digits surrounded by 2 or more braces on both sides
-    # Match strings like: {{5}}, {{{{{4}}, {{2}}}}}
-    macro_escaped_arg_pattern = re.compile(r'{{2}\d+}{2}')
-
-    # Finds a string of digits
-    digit_pattern = re.compile(r'\d+')
-
-
-@dataclass(frozen=True)
-class Macro:
-    """Defines a cmd2 macro"""
-
-    # Name of the macro
-    name: str
-
-    # The string the macro resolves to
-    value: str
-
-    # The minimum number of args the user has to pass to this macro
-    minimum_arg_count: int
-
-    # Used to fill in argument placeholders in the macro
-    arg_list: List[MacroArg] = field(default_factory=list)
-
-
 @dataclass(frozen=True)
 class Statement(str):  # type: ignore[override]
     """String subclass with additional attributes to store the results of parsing.
@@ -210,10 +160,10 @@ def expanded_command_line(self) -> str:
     def argv(self) -> List[str]:
         """a list of arguments a-la ``sys.argv``.
 
-        The first element of the list is the command after shortcut and macro
-        expansion. Subsequent elements of the list contain any additional
-        arguments, with quotes removed, just like bash would. This is very
-        useful if you are going to use ``argparse.parse_args()``.
+        The first element of the list is the command after shortcut expansion.
+        Subsequent elements of the list contain any additional arguments,
+        with quotes removed, just like bash would. This is very useful if
+        you are going to use ``argparse.parse_args()``.
 
         If you want to strip quotes from the input, you can use ``argv[1:]``.
         """
diff --git a/docs/api/cmd.rst b/docs/api/cmd.rst
index 4fbb8ccd5..fef2c78c7 100644
--- a/docs/api/cmd.rst
+++ b/docs/api/cmd.rst
@@ -9,7 +9,7 @@ cmd2.Cmd
     .. attribute:: default_error
 
       The error message displayed when a non-existent command is run.
-      Default: ``{} is not a recognized command, alias, or macro.``
+      Default: ``{} is not a recognized command or alias.``
 
     .. attribute:: help_error
 
diff --git a/docs/api/parsing.rst b/docs/api/parsing.rst
index fa726700b..490e56559 100644
--- a/docs/api/parsing.rst
+++ b/docs/api/parsing.rst
@@ -15,7 +15,7 @@ Classes for parsing and storing user input.
 
     .. attribute:: command
 
-      The name of the command after shortcuts and macros have been expanded
+      The name of the command after shortcuts have been expanded
 
     .. attribute:: args
 
diff --git a/docs/examples/first_app.rst b/docs/examples/first_app.rst
index d90f96d86..b0164f81d 100644
--- a/docs/examples/first_app.rst
+++ b/docs/examples/first_app.rst
@@ -11,7 +11,7 @@ features of ``cmd2``:
 * :ref:`features/argument_processing:Argument Processing`
 * :ref:`features/generating_output:Generating Output`
 * :ref:`features/help:Help`
-* :ref:`features/shortcuts_aliases_macros:Shortcuts`
+* :ref:`features/shortcuts_aliases:Shortcuts`
 * :ref:`features/multiline_commands:Multiline Commands`
 * :ref:`features/history:History`
 
@@ -178,8 +178,8 @@ Shortcuts
 ---------
 
 ``cmd2`` has several capabilities to simplify repetitive user input:
-:ref:`Shortcuts, Aliases, and Macros
-<features/shortcuts_aliases_macros:Shortcuts, Aliases, and Macros>`. Let's add
+:ref:`Shortcuts and Aliases
+<features/shortcuts_aliases:Shortcuts and Aliases>`. Let's add
 a shortcut to our application. Shortcuts are character strings that can be used
 instead of a command name. For example, ``cmd2`` has support for a shortcut
 ``!`` which runs the ``shell`` command. So instead of typing this:
diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst
index 4925fc3d7..93d98a7eb 100644
--- a/docs/features/builtin_commands.rst
+++ b/docs/features/builtin_commands.rst
@@ -13,7 +13,7 @@ alias
 ~~~~~
 
 This command manages aliases via subcommands ``create``, ``delete``, and
-``list``.  See :ref:`features/shortcuts_aliases_macros:Aliases` for more
+``list``.  See :ref:`features/shortcuts_aliases:Aliases` for more
 information.
 
 edit
@@ -50,14 +50,6 @@ ipy
 This optional opt-in command enters an interactive IPython shell.  See
 :ref:`features/embedded_python_shells:IPython (optional)` for more information.
 
-macro
-~~~~~
-
-This command manages macros via subcommands ``create``, ``delete``, and
-``list``.  A macro is similar to an alias, but it can contain argument
-placeholders.  See :ref:`features/shortcuts_aliases_macros:Macros` for more
-information.
-
 py
 ~~
 
@@ -143,7 +135,7 @@ shortcuts
 ~~~~~~~~~
 
 This command lists available shortcuts.  See
-:ref:`features/shortcuts_aliases_macros:Shortcuts` for more information.
+:ref:`features/shortcuts_aliases:Shortcuts` for more information.
 
 
 Remove Builtin Commands
diff --git a/docs/features/commands.rst b/docs/features/commands.rst
index 5dd5a163b..1b3da218a 100644
--- a/docs/features/commands.rst
+++ b/docs/features/commands.rst
@@ -67,7 +67,7 @@ the cmd_ module. This parsing handles:
 - quoted arguments
 - output redirection and piping
 - multi-line commands
-- shortcut, macro, and alias expansion
+- shortcut and alias expansion
 
 In addition to parsing all of these elements from the user input, ``cmd2`` also
 has code to make all of these items work; it's almost transparent to you and to
diff --git a/docs/features/help.rst b/docs/features/help.rst
index b98e4164d..759a5f150 100644
--- a/docs/features/help.rst
+++ b/docs/features/help.rst
@@ -19,8 +19,8 @@ of the commands available:
 
     Documented commands (use 'help -v' for verbose/'help <topic>' for details):
     ===========================================================================
-    alias  help     ipy    py    run_pyscript  set    shortcuts
-    edit   history  macro  quit  run_script    shell
+    alias  help     ipy  quit          run_script  shell
+    edit   history  py   run_pyscript  set         shortcuts
 
 The ``help`` command can also be used to provide detailed help for a specific
 command:
@@ -63,8 +63,8 @@ By default, the ``help`` command displays::
 
   Documented commands (use 'help -v' for verbose/'help <topic>' for details):
   ===========================================================================
-  alias  help     ipy    py    run_pyscript  set    shortcuts
-  edit   history  macro  quit  run_script    shell
+  alias  help     ipy  quit          run_script  shell
+  edit   history  py   run_pyscript  set         shortcuts
 
 If you have a large number of commands, you can optionally group your commands
 into categories. Here's the output from the example ``help_categories.py``::
@@ -90,8 +90,8 @@ into categories. Here's the output from the example ``help_categories.py``::
 
   Other
   =====
-  alias   edit  history  py    run_pyscript  set    shortcuts
-  config  help  macro    quit  run_script    shell  version
+  alias   edit  history  run_pyscript  set    shortcuts
+  config  help  quit     run_script    shell  version
 
 There are 2 methods of specifying command categories, using the
 ``@with_category`` decorator or with the ``categorize()`` function. Once a
@@ -142,51 +142,49 @@ The ``help`` command also has a verbose option (``help -v`` or ``help
     Documented commands (use 'help -v' for verbose/'help <topic>' for details):
 
     Application Management
-    ================================================================================
-    deploy              Deploy command
-    expire              Expire command
-    findleakers         Find Leakers command
-    list                List command
-    redeploy            Redeploy command
-    restart             usage: restart [-h] {now,later,sometime,whenever}
-    sessions            Sessions command
-    start               Start command
-    stop                Stop command
-    undeploy            Undeploy command
+    ======================================================================================================
+    deploy                Deploy command
+    expire                Expire command
+    findleakers           Find Leakers command
+    list                  List command
+    redeploy              Redeploy command
+    restart               Restart command
+    sessions              Sessions command
+    start                 Start command
+    stop                  Stop command
+    undeploy              Undeploy command
 
     Connecting
-    ================================================================================
-    connect             Connect command
-    which               Which command
+    ======================================================================================================
+    connect               Connect command
+    which                 Which command
 
     Server Information
-    ================================================================================
-    resources              Resources command
-    serverinfo             Server Info command
-    sslconnectorciphers    SSL Connector Ciphers command is an example of a command that contains
-                           multiple lines of help information for the user. Each line of help in a
-                           contiguous set of lines will be printed and aligned in the verbose output
-                           provided with 'help --verbose'
-    status                 Status command
-    thread_dump            Thread Dump command
-    vminfo                 VM Info command
+    ======================================================================================================
+    resources             Resources command
+    serverinfo            Server Info command
+    sslconnectorciphers   SSL Connector Ciphers command is an example of a command that contains
+                          multiple lines of help information for the user. Each line of help in a
+                          contiguous set of lines will be printed and aligned in the verbose output
+                          provided with 'help --verbose'
+    status                Status command
+    thread_dump           Thread Dump command
+    vminfo                VM Info command
 
     Other
-    ================================================================================
-    alias               Manage aliases
-    config              Config command
-    edit                Run a text editor and optionally open a file with it
-    help                List available commands or provide detailed help for a specific command
-    history             View, run, edit, save, or clear previously entered commands
-    macro               Manage macros
-    py                  Invoke Python command or shell
-    quit                Exits this application
-    run_pyscript        Runs a python script file inside the console
-    run_script          Runs commands in script file that is encoded as either ASCII or UTF-8 text
-    set                 Set a settable parameter or show current settings of parameters
-    shell               Execute a command as if at the OS prompt
-    shortcuts           List available shortcuts
-    version             Version command
+    ======================================================================================================
+    alias                 Manage aliases
+    config                Config command
+    edit                  Run a text editor and optionally open a file with it
+    help                  List available commands or provide detailed help for a specific command
+    history               View, run, edit, save, or clear previously entered commands
+    quit                  Exit this application
+    run_pyscript          Run a Python script file inside the console
+    run_script            Run commands in script file that is encoded as either ASCII or UTF-8 text.
+    set                   Set a settable parameter or show current settings of parameters
+    shell                 Execute a command as if at the OS prompt
+    shortcuts             List available shortcuts
+    version               Version command
 
 When called with the ``-v`` flag for verbose help, the one-line description for
 each command is provided by the first line of the docstring for that command's
diff --git a/docs/features/history.rst b/docs/features/history.rst
index 056e02a0b..e421588aa 100644
--- a/docs/features/history.rst
+++ b/docs/features/history.rst
@@ -232,9 +232,9 @@ clipboard::
 
     (Cmd) history -s 1:3
 
-``cmd2`` supports both aliases and macros, which allow you to substitute a
-short, more convenient input string with a longer replacement string. Say we
-create an alias like this, and then use it::
+``cmd2`` supports aliases which allow you to substitute a short, more
+convenient input string with a longer replacement string. Say we create
+an alias like this, and then use it::
 
     (Cmd) alias create ls shell ls -aF
     Alias 'ls' created
@@ -248,7 +248,7 @@ By default, the ``history`` command shows exactly what we typed::
         2  ls -d h*
 
 There are two ways to modify that display so you can see what aliases and
-macros were expanded to. The first is to use ``-x`` or ``--expanded``. These
+shortcuts were expanded to. The first is to use ``-x`` or ``--expanded``. These
 options show the expanded command instead of the entered command::
 
     (Cmd) history -x
@@ -264,6 +264,6 @@ If you want to see both the entered command and the expanded command, use the
         2x shell ls -aF -d h*
 
 If the entered command had no expansion, it is displayed as usual. However, if
-there is some change as the result of expanding macros and aliases, then the
-entered command is displayed with the number, and the expanded command is
-displayed with the number followed by an ``x``.
+there is some change as the result of expanding aliases, then the entered
+command is displayed with the number, and the expanded command is displayed
+with the number followed by an ``x``.
diff --git a/docs/features/index.rst b/docs/features/index.rst
index 48590b6ad..8e6fc595b 100644
--- a/docs/features/index.rst
+++ b/docs/features/index.rst
@@ -26,7 +26,7 @@ Features
    redirection
    scripting
    settings
-   shortcuts_aliases_macros
+   shortcuts_aliases
    startup_commands
    table_creation
    transcripts
diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst
index 3ee96cf9e..b06f65266 100644
--- a/docs/features/initialization.rst
+++ b/docs/features/initialization.rst
@@ -141,7 +141,6 @@ override:
   of results in a Python script or interactive console. Built-in commands don't
   make use of this.  It is purely there for user-defined commands and
   convenience.
-- **macros**: dictionary of macro names and their values
 - **max_completion_items**: max number of CompletionItems to display during
   tab completion (Default: 50)
 - **pager**: sets the pager command used by the ``Cmd.ppaged()`` method for
diff --git a/docs/features/os.rst b/docs/features/os.rst
index 77bc6a668..722e761ad 100644
--- a/docs/features/os.rst
+++ b/docs/features/os.rst
@@ -14,7 +14,7 @@ operating system shell::
 
     (Cmd) shell ls -al
 
-If you use the default :ref:`features/shortcuts_aliases_macros:Shortcuts`
+If you use the default :ref:`features/shortcuts_aliases:Shortcuts`
 defined in ``cmd2`` you'll get a ``!`` shortcut for ``shell``, which allows you
 to type::
 
@@ -107,8 +107,8 @@ loop::
 
     Documented commands (use 'help -v' for verbose/'help <topic>' for details):
     ===========================================================================
-    alias  help     macro   orate  quit          run_script  set    shortcuts
-    edit   history  mumble  py     run_pyscript  say         shell  speak
+    alias  help     ipy  quit          run_script  shell
+    edit   history  py   run_pyscript  set         shortcuts
 
     (Cmd)
 
diff --git a/docs/features/shortcuts_aliases_macros.rst b/docs/features/shortcuts_aliases.rst
similarity index 57%
rename from docs/features/shortcuts_aliases_macros.rst
rename to docs/features/shortcuts_aliases.rst
index 58d6d83cd..243257feb 100644
--- a/docs/features/shortcuts_aliases_macros.rst
+++ b/docs/features/shortcuts_aliases.rst
@@ -1,5 +1,5 @@
-Shortcuts, Aliases, and Macros
-==============================
+Shortcuts and Aliases
+=====================
 
 Shortcuts
 ---------
@@ -38,7 +38,7 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the
   updating the ``shortcuts`` attribute  This warning applies in general to many
   other attributes which are not settable at runtime.
 
-Note: Command, alias, and macro names cannot start with a shortcut
+Note: Command and alias names cannot start with a shortcut
 
 Aliases
 -------
@@ -74,44 +74,4 @@ Use ``alias delete`` to remove aliases
 
 For more details run: ``help alias delete``
 
-Note: Aliases cannot have the same name as a command or macro
-
-Macros
-------
-
-``cmd2`` provides a feature that is similar to aliases called macros. The major
-difference between macros and aliases is that macros can contain argument
-placeholders. Arguments are expressed when creating a macro using {#} notation
-where {1} means the first argument.
-
-The following creates a macro called my_macro that expects two arguments:
-
-  macro create my_macro make_dinner -meat {1} -veggie {2}
-
-When the macro is called, the provided arguments are resolved and the assembled
-command is run. For example:
-
-  my_macro beef broccoli ---> make_dinner -meat beef -veggie broccoli
-
-Similar to aliases, pipes and redirectors need to be quoted in the definition
-of a macro::
-
-    macro create lc !cat "{1}" "|" less
-
-To use the literal string ``{1}`` in your command, escape it this way:
-``{{1}}``.  Because macros do not resolve until after hitting ``<Enter>``,
-tab completion will only complete paths while typing a macro.
-
-
-For more details run: ``help macro create``
-
-The macro command has ``list`` and ``delete`` subcommands that function
-identically to the alias subcommands of the same name. Like aliases, macros can
-be created via a ``cmd2`` startup script to preserve them across application
-sessions.
-
-For more details on listing macros run: ``help macro list``
-
-For more details on deleting macros run: ``help macro delete``
-
-Note: Macros cannot have the same name as a command or alias
+Note: Aliases cannot have the same name as a command
diff --git a/docs/migrating/incompatibilities.rst b/docs/migrating/incompatibilities.rst
index ba6f2ed10..bc922c380 100644
--- a/docs/migrating/incompatibilities.rst
+++ b/docs/migrating/incompatibilities.rst
@@ -38,7 +38,7 @@ characters in command names while simultaneously using ``identchars``
 functionality can be somewhat painful. Requiring white space to delimit
 arguments also ensures reliable operation of many other useful ``cmd2``
 features, including :ref:`features/completion:Completion` and
-:ref:`features/shortcuts_aliases_macros:Shortcuts, Aliases, and Macros`.
+:ref:`features/shortcuts_aliases:Shortcuts and Aliases`.
 
 If you really need this functionality in your app, you can add it back in by
 writing a :ref:`Postparsing Hook <features/hooks:Postparsing Hooks>`.
diff --git a/docs/migrating/why.rst b/docs/migrating/why.rst
index 2bfd45f14..bbfccb650 100644
--- a/docs/migrating/why.rst
+++ b/docs/migrating/why.rst
@@ -55,8 +55,8 @@ new features and capabilities, without you having to do anything:
 - Users can load script files, which contain a series of commands
   to be executed.
 
-- Users can create :ref:`features/shortcuts_aliases_macros:Shortcuts, Aliases,
-  and Macros` to reduce the typing required for repetitive commands.
+- Users can create :ref:`features/shortcuts_aliases:Shortcuts and Aliases`
+  to reduce the typing required for repetitive commands.
 
 - Embedded python shell allows a user to execute python code from within your
   ``cmd2`` app. How meta.
diff --git a/examples/help_categories.py b/examples/help_categories.py
index 5c349422c..8059ca90a 100755
--- a/examples/help_categories.py
+++ b/examples/help_categories.py
@@ -37,6 +37,9 @@ class HelpCategories(cmd2.Cmd):
     def __init__(self):
         super().__init__()
 
+        # Set the default category for uncategorized commands
+        self.default_category = 'Other'
+
     def do_connect(self, _):
         """Connect command"""
         self.poutput('Connect')
diff --git a/tests/conftest.py b/tests/conftest.py
index 644ae7cca..0b3a01786 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -81,8 +81,7 @@ def verify_help_text(
 formatting:
   -s, --script          output commands in script format, i.e. without command
                         numbers
-  -x, --expanded        output fully parsed commands with any aliases and
-                        macros expanded, instead of typed commands
+  -x, --expanded        output fully parsed commands with aliases and shortcuts expanded
   -v, --verbose         display history and include expanded commands if they
                         differ from the typed command
   -a, --all             display all commands, including ones persisted from
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 374ba10a2..efa4868b5 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1630,8 +1630,8 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app):
     assert statement.terminator == ';'
 
 
-def test_multiline_input_line_to_statement(multiline_app):
-    # Verify _input_line_to_statement saves the fully entered input line for multiline commands
+def test_multiline_complete_statement(multiline_app):
+    # Verify _complete_statement saves the fully entered input line for multiline commands
 
     # Mock out the input call so we don't actually wait for a user's response
     # on stdin when it looks for more input
@@ -1639,7 +1639,7 @@ def test_multiline_input_line_to_statement(multiline_app):
     builtins.input = m
 
     line = 'orate hi'
-    statement = multiline_app._input_line_to_statement(line)
+    statement = multiline_app._complete_statement(line)
     assert statement.raw == 'orate hi\nperson\n'
     assert statement == 'hi person'
     assert statement.command == 'orate'
@@ -1998,8 +1998,7 @@ def test_poutput_ansi_never(outsim_app):
     assert out == expected
 
 
-# These are invalid names for aliases and macros
-invalid_command_name = [
+invalid_alias_names = [
     '""',  # Blank name
     constants.COMMENT_CHAR,
     '!no_shortcut',
@@ -2025,19 +2024,6 @@ def test_get_alias_completion_items(base_app):
         assert cur_res.description.rstrip() == base_app.aliases[cur_res]
 
 
-def test_get_macro_completion_items(base_app):
-    run_cmd(base_app, 'macro create foo !echo foo')
-    run_cmd(base_app, 'macro create bar !echo bar')
-
-    results = base_app._get_macro_completion_items()
-    assert len(results) == len(base_app.macros)
-
-    for cur_res in results:
-        assert cur_res in base_app.macros
-        # Strip trailing spaces from table output
-        assert cur_res.description.rstrip() == base_app.macros[cur_res].value
-
-
 def test_get_settable_completion_items(base_app):
     results = base_app._get_settable_completion_items()
     assert len(results) == len(base_app.settables)
@@ -2113,7 +2099,7 @@ def test_alias_create_with_quoted_tokens(base_app):
     assert base_app.last_result[alias_name] == alias_command
 
 
-@pytest.mark.parametrize('alias_name', invalid_command_name)
+@pytest.mark.parametrize('alias_name', invalid_alias_names)
 def test_alias_create_invalid_name(base_app, alias_name, capsys):
     out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name))
     assert "Invalid alias name" in err[0]
@@ -2126,14 +2112,6 @@ def test_alias_create_with_command_name(base_app):
     assert base_app.last_result is False
 
 
-def test_alias_create_with_macro_name(base_app):
-    macro = "my_macro"
-    run_cmd(base_app, 'macro create {} help'.format(macro))
-    out, err = run_cmd(base_app, 'alias create {} help'.format(macro))
-    assert "Alias cannot have the same name as a macro" in err[0]
-    assert base_app.last_result is False
-
-
 def test_alias_that_resolves_into_comment(base_app):
     # Create the alias
     out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
@@ -2192,228 +2170,6 @@ def test_multiple_aliases(base_app):
     verify_help_text(base_app, out)
 
 
-def test_macro_no_subcommand(base_app):
-    out, err = run_cmd(base_app, 'macro')
-    assert "Usage: macro [-h]" in err[0]
-    assert "Error: the following arguments are required: SUBCOMMAND" in err[1]
-
-
-def test_macro_create(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake run_pyscript')
-    assert out == normalize("Macro 'fake' created")
-    assert base_app.last_result is True
-
-    # Use the macro
-    out, err = run_cmd(base_app, 'fake')
-    assert "the following arguments are required: script_path" in err[1]
-
-    # See a list of macros
-    out, err = run_cmd(base_app, 'macro list')
-    assert out == normalize('macro create fake run_pyscript')
-    assert len(base_app.last_result) == len(base_app.macros)
-    assert base_app.last_result['fake'] == "run_pyscript"
-
-    # Look up the new macro
-    out, err = run_cmd(base_app, 'macro list fake')
-    assert out == normalize('macro create fake run_pyscript')
-    assert len(base_app.last_result) == 1
-    assert base_app.last_result['fake'] == "run_pyscript"
-
-    # Overwrite macro
-    out, err = run_cmd(base_app, 'macro create fake help')
-    assert out == normalize("Macro 'fake' overwritten")
-    assert base_app.last_result is True
-
-    # Look up the updated macro
-    out, err = run_cmd(base_app, 'macro list fake')
-    assert out == normalize('macro create fake help')
-    assert len(base_app.last_result) == 1
-    assert base_app.last_result['fake'] == "help"
-
-
-def test_macro_create_with_quoted_tokens(base_app):
-    """Demonstrate that quotes in macro value will be preserved"""
-    macro_name = "fake"
-    macro_command = 'help ">" "out file.txt" ";"'
-    create_command = f"macro create {macro_name} {macro_command}"
-
-    # Create the macro
-    out, err = run_cmd(base_app, create_command)
-    assert out == normalize("Macro 'fake' created")
-
-    # Look up the new macro and verify all quotes are preserved
-    out, err = run_cmd(base_app, 'macro list fake')
-    assert out == normalize(create_command)
-    assert len(base_app.last_result) == 1
-    assert base_app.last_result[macro_name] == macro_command
-
-
-@pytest.mark.parametrize('macro_name', invalid_command_name)
-def test_macro_create_invalid_name(base_app, macro_name):
-    out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name))
-    assert "Invalid macro name" in err[0]
-    assert base_app.last_result is False
-
-
-def test_macro_create_with_command_name(base_app):
-    out, err = run_cmd(base_app, 'macro create help stuff')
-    assert "Macro cannot have the same name as a command" in err[0]
-    assert base_app.last_result is False
-
-
-def test_macro_create_with_alias_name(base_app):
-    macro = "my_macro"
-    run_cmd(base_app, 'alias create {} help'.format(macro))
-    out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
-    assert "Macro cannot have the same name as an alias" in err[0]
-    assert base_app.last_result is False
-
-
-def test_macro_create_with_args(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake {1} {2}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Run the macro
-    out, err = run_cmd(base_app, 'fake help -v')
-    verify_help_text(base_app, out)
-
-
-def test_macro_create_with_escaped_args(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake help {{1}}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Run the macro
-    out, err = run_cmd(base_app, 'fake')
-    assert err[0].startswith('No help on {1}')
-
-
-def test_macro_usage_with_missing_args(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake help {1} {2}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Run the macro
-    out, err = run_cmd(base_app, 'fake arg1')
-    assert "expects at least 2 arguments" in err[0]
-
-
-def test_macro_usage_with_exta_args(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake help {1}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Run the macro
-    out, err = run_cmd(base_app, 'fake alias create')
-    assert "Usage: alias create" in out[0]
-
-
-def test_macro_create_with_missing_arg_nums(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake help {1} {3}')
-    assert "Not all numbers between 1 and 3" in err[0]
-    assert base_app.last_result is False
-
-
-def test_macro_create_with_invalid_arg_num(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}')
-    assert "Argument numbers must be greater than 0" in err[0]
-    assert base_app.last_result is False
-
-
-def test_macro_create_with_unicode_numbered_arg(base_app):
-    # Create the macro expecting 1 argument
-    out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Run the macro
-    out, err = run_cmd(base_app, 'fake')
-    assert "expects at least 1 argument" in err[0]
-
-
-def test_macro_create_with_missing_unicode_arg_nums(base_app):
-    out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}')
-    assert "Not all numbers between 1 and 3" in err[0]
-    assert base_app.last_result is False
-
-
-def test_macro_that_resolves_into_comment(base_app):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake {1} blah blah')
-    assert out == normalize("Macro 'fake' created")
-
-    # Use the macro
-    out, err = run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
-    assert not out
-    assert not err
-
-
-def test_macro_list_invalid_macro(base_app):
-    # Look up invalid macro
-    out, err = run_cmd(base_app, 'macro list invalid')
-    assert "Macro 'invalid' not found" in err[0]
-    assert base_app.last_result == {}
-
-
-def test_macro_delete(base_app):
-    # Create an macro
-    run_cmd(base_app, 'macro create fake run_pyscript')
-
-    # Delete the macro
-    out, err = run_cmd(base_app, 'macro delete fake')
-    assert out == normalize("Macro 'fake' deleted")
-    assert base_app.last_result is True
-
-
-def test_macro_delete_all(base_app):
-    out, err = run_cmd(base_app, 'macro delete --all')
-    assert out == normalize("All macros deleted")
-    assert base_app.last_result is True
-
-
-def test_macro_delete_non_existing(base_app):
-    out, err = run_cmd(base_app, 'macro delete fake')
-    assert "Macro 'fake' does not exist" in err[0]
-    assert base_app.last_result is True
-
-
-def test_macro_delete_no_name(base_app):
-    out, err = run_cmd(base_app, 'macro delete')
-    assert "Either --all or macro name(s)" in err[0]
-    assert base_app.last_result is False
-
-
-def test_multiple_macros(base_app):
-    macro1 = 'h1'
-    macro2 = 'h2'
-    run_cmd(base_app, 'macro create {} help'.format(macro1))
-    run_cmd(base_app, 'macro create {} help -v'.format(macro2))
-    out, err = run_cmd(base_app, macro1)
-    verify_help_text(base_app, out)
-
-    out2, err2 = run_cmd(base_app, macro2)
-    verify_help_text(base_app, out2)
-    assert len(out2) > len(out)
-
-
-def test_nonexistent_macro(base_app):
-    from cmd2.parsing import (
-        StatementParser,
-    )
-
-    exception = None
-
-    try:
-        base_app._resolve_macro(StatementParser().parse('fake'))
-    except KeyError as e:
-        exception = e
-
-    assert exception is not None
-
-
 @with_ansi_style(ansi.AllowStyle.ALWAYS)
 def test_perror_style(base_app, capsys):
     msg = 'testing...'
@@ -2567,7 +2323,6 @@ def test_get_all_commands(base_app):
         'help',
         'history',
         'ipy',
-        'macro',
         'py',
         'quit',
         'run_pyscript',
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 18b7c0f27..2bb08d3a5 100755
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -26,8 +26,6 @@
 
 from .conftest import (
     complete_tester,
-    normalize,
-    run_cmd,
 )
 
 # List of strings used with completion functions
@@ -186,25 +184,6 @@ def test_complete_exception(cmd2_app, capsys):
     assert "IndexError" in err
 
 
-def test_complete_macro(base_app, request):
-    # Create the macro
-    out, err = run_cmd(base_app, 'macro create fake run_pyscript {1}')
-    assert out == normalize("Macro 'fake' created")
-
-    # Macros do path completion
-    test_dir = os.path.dirname(request.module.__file__)
-
-    text = os.path.join(test_dir, 's')
-    line = 'fake {}'.format(text)
-
-    endidx = len(line)
-    begidx = endidx - len(text)
-
-    expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep]
-    first_match = complete_tester(text, line, begidx, endidx, base_app)
-    assert first_match is not None and base_app.completion_matches == expected
-
-
 def test_default_sort_key(cmd2_app):
     text = ''
     line = 'test_sort_key {}'.format(text)
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index e3d42d7c7..ed5a00f99 100755
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -1034,111 +1034,3 @@ def test_is_valid_command_valid(parser):
     valid, errmsg = parser.is_valid_command('!subcmd', is_subcommand=True)
     assert valid
     assert not errmsg
-
-
-def test_macro_normal_arg_pattern():
-    # This pattern matches digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
-    from cmd2.parsing import (
-        MacroArg,
-    )
-
-    pattern = MacroArg.macro_normal_arg_pattern
-
-    # Valid strings
-    matches = pattern.findall('{5}')
-    assert matches == ['{5}']
-
-    matches = pattern.findall('{233}')
-    assert matches == ['{233}']
-
-    matches = pattern.findall('{{{{{4}')
-    assert matches == ['{4}']
-
-    matches = pattern.findall('{2}}}}}')
-    assert matches == ['{2}']
-
-    matches = pattern.findall('{3}{4}{5}')
-    assert matches == ['{3}', '{4}', '{5}']
-
-    matches = pattern.findall('{3} {4} {5}')
-    assert matches == ['{3}', '{4}', '{5}']
-
-    matches = pattern.findall('{3} {{{4} {5}}}}')
-    assert matches == ['{3}', '{4}', '{5}']
-
-    matches = pattern.findall('{3} text {4} stuff {5}}}}')
-    assert matches == ['{3}', '{4}', '{5}']
-
-    # Unicode digit
-    matches = pattern.findall('{\N{ARABIC-INDIC DIGIT ONE}}')
-    assert matches == ['{\N{ARABIC-INDIC DIGIT ONE}}']
-
-    # Invalid strings
-    matches = pattern.findall('5')
-    assert not matches
-
-    matches = pattern.findall('{5')
-    assert not matches
-
-    matches = pattern.findall('5}')
-    assert not matches
-
-    matches = pattern.findall('{{5}}')
-    assert not matches
-
-    matches = pattern.findall('{5text}')
-    assert not matches
-
-
-def test_macro_escaped_arg_pattern():
-    # This pattern matches digits surrounded by 2 or more braces on both sides
-    from cmd2.parsing import (
-        MacroArg,
-    )
-
-    pattern = MacroArg.macro_escaped_arg_pattern
-
-    # Valid strings
-    matches = pattern.findall('{{5}}')
-    assert matches == ['{{5}}']
-
-    matches = pattern.findall('{{233}}')
-    assert matches == ['{{233}}']
-
-    matches = pattern.findall('{{{{{4}}')
-    assert matches == ['{{4}}']
-
-    matches = pattern.findall('{{2}}}}}')
-    assert matches == ['{{2}}']
-
-    matches = pattern.findall('{{3}}{{4}}{{5}}')
-    assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
-    matches = pattern.findall('{{3}} {{4}} {{5}}')
-    assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
-    matches = pattern.findall('{{3}} {{{4}} {{5}}}}')
-    assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
-    matches = pattern.findall('{{3}} text {{4}} stuff {{5}}}}')
-    assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
-    # Unicode digit
-    matches = pattern.findall('{{\N{ARABIC-INDIC DIGIT ONE}}}')
-    assert matches == ['{{\N{ARABIC-INDIC DIGIT ONE}}}']
-
-    # Invalid strings
-    matches = pattern.findall('5')
-    assert not matches
-
-    matches = pattern.findall('{{5')
-    assert not matches
-
-    matches = pattern.findall('5}}')
-    assert not matches
-
-    matches = pattern.findall('{5}')
-    assert not matches
-
-    matches = pattern.findall('{{5text}}')
-    assert not matches
diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py
index c8c6d34b4..e70185a89 100644
--- a/tests_isolated/test_commandset/conftest.py
+++ b/tests_isolated/test_commandset/conftest.py
@@ -83,8 +83,7 @@ def verify_help_text(
 formatting:
   -s, --script          output commands in script format, i.e. without command
                         numbers
-  -x, --expanded        output fully parsed commands with any aliases and
-                        macros expanded, instead of typed commands
+  -x, --expanded        output fully parsed commands with aliases and shortcuts expanded
   -v, --verbose         display history and include expanded commands if they
                         differ from the typed command
   -a, --all             display all commands, including ones persisted from
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index c7293d411..7064b3dba 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -299,17 +299,17 @@ def test_load_commandset_errors(command_sets_manual, capsys):
 
     delattr(command_sets_manual, 'do_durian')
 
-    # pre-create intentionally conflicting macro and alias names
-    command_sets_manual.app_cmd('macro create apple run_pyscript')
+    # pre-create intentionally conflicting aliases
+    command_sets_manual.app_cmd('alias create apple run_pyscript')
     command_sets_manual.app_cmd('alias create banana run_pyscript')
 
     # now install a command set and verify the commands are now present
     command_sets_manual.register_command_set(cmd_set)
     out, err = capsys.readouterr()
 
-    # verify aliases and macros are deleted with warning if they conflict with a command
+    # verify aliases are deleted with warning if they conflict with a command
+    assert "Deleting alias 'apple'" in err
     assert "Deleting alias 'banana'" in err
-    assert "Deleting macro 'apple'" in err
 
     # verify duplicate commands are detected
     with pytest.raises(CommandSetRegistrationError):