Skip to content

Restore macros #1464

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

Merged
merged 2 commits into from
Jul 17, 2025
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

- Breaking Changes

- Removed macros
- No longer setting parser's `prog` value in `with_argparser()` since it gets set in
`Cmd._build_parser()`. This code had previously been restored to support backward
compatibility in `cmd2` 2.0 family.

- Enhancements

- Simplified the process to set a custom parser for `cmd2's` built-in commands. See
[custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py)
example for more details.

- Added `Cmd.macro_arg_complete()` which tab completes arguments to a macro. Its default
behavior is to perform path completion, but it can be overridden as needed.

## 2.7.0 (June 30, 2025)

- Enhancements
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ first pillar of 'ease of command discovery'. The following is a list of features

<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 creation, command
line argument parsing and execution of cmd2 scripting.
cmd2 creates the second pillar of 'ease of transition to automation' through alias/macro creation,
command line argument parsing and execution of cmd2 scripting.

- Flexible alias creation for quick abstraction of commands.
- Flexible alias and macro 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
Expand Down
436 changes: 423 additions & 13 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

58 changes: 54 additions & 4 deletions cmd2/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,56 @@ 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] # noqa: SLOT000
"""String subclass with additional attributes to store the results of parsing.
Expand Down Expand Up @@ -155,10 +205,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 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 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()``.

If you want to strip quotes from the input, you can use ``argv[1:]``.
"""
Expand Down
9 changes: 5 additions & 4 deletions docs/examples/first_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Here's a quick walkthrough of a simple application which demonstrates 8 features
- [Argument Processing](../features/argument_processing.md)
- [Generating Output](../features/generating_output.md)
- [Help](../features/help.md)
- [Shortcuts](../features/shortcuts_aliases.md#shortcuts)
- [Shortcuts](../features/shortcuts_aliases_macros.md#shortcuts)
- [Multiline Commands](../features/multiline_commands.md)
- [History](../features/history.md)

Expand Down Expand Up @@ -166,9 +166,10 @@ With those few lines of code, we created a [command](../features/commands.md), u
## Shortcuts

`cmd2` has several capabilities to simplify repetitive user input:
[Shortcuts and Aliases](../features/shortcuts_aliases.md). 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:
[Shortcuts, Aliases, and Macros](../features/shortcuts_aliases_macros.md). 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:

```shell
(Cmd) shell ls -al
Expand Down
12 changes: 9 additions & 3 deletions docs/features/builtin_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ to be part of the application.
### alias

This command manages aliases via subcommands `create`, `delete`, and `list`. See
[Aliases](shortcuts_aliases.md#aliases) for more information.
[Aliases](shortcuts_aliases_macros.md#aliases) for more information.

### edit

Expand Down Expand Up @@ -38,6 +38,12 @@ history. See [History](history.md) for more information.
This optional opt-in command enters an interactive IPython shell. See
[IPython (optional)](./embedded_python_shells.md#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 [Macros](./shortcuts_aliases_macros.md#macros)
for more information.

### py

This command invokes a Python command or shell. See
Expand Down Expand Up @@ -108,8 +114,8 @@ Execute a command as if at the operating system shell prompt:

### shortcuts

This command lists available shortcuts. See [Shortcuts](./shortcuts_aliases.md#shortcuts) for more
information.
This command lists available shortcuts. See [Shortcuts](./shortcuts_aliases_macros.md#shortcuts) for
more information.

## Remove Builtin Commands

Expand Down
2 changes: 1 addition & 1 deletion docs/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ backwards compatibility.
- quoted arguments
- output redirection and piping
- multi-line commands
- shortcut and alias expansion
- shortcut, alias, and macro 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 the commands you write in your own
Expand Down
23 changes: 12 additions & 11 deletions docs/features/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ command. The `help` command by itself displays a list of the commands available:

Documented commands (use 'help -v' for verbose/'help <topic>' for details):
===========================================================================
alias help ipy quit run_script shell
edit history py run_pyscript set shortcuts
alias help ipy py run_pyscript set shortcuts
edit history macro quit run_script shell
```

The `help` command can also be used to provide detailed help for a specific command:
Expand Down Expand Up @@ -53,8 +53,8 @@ By default, the `help` command displays:

Documented commands (use 'help -v' for verbose/'help <topic>' for details):
===========================================================================
alias help ipy quit run_script shell
edit history py run_pyscript set shortcuts
alias help ipy py run_pyscript set shortcuts
edit history macro quit run_script shell

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`:
Expand All @@ -80,8 +80,8 @@ Here's the output from the example `help_categories.py`:

Other
=====
alias edit history run_pyscript set shortcuts
config help quit run_script shell version
alias edit history py run_pyscript set shortcuts
config help macro 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 single command category is detected, the help output switches to
Expand Down Expand Up @@ -143,9 +143,9 @@ categories with per-command Help Messages:
findleakers Find Leakers command.
list List command.
redeploy Redeploy command.
restart Restart
restart Restart command.
sessions Sessions command.
start Start
start Start command.
stop Stop command.
undeploy Undeploy command.

Expand All @@ -164,9 +164,9 @@ categories with per-command Help Messages:
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'.
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.
Expand All @@ -178,6 +178,7 @@ categories with per-command Help Messages:
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.
quit Exit this application.
run_pyscript Run Python script within this application's environment.
run_script Run text script.
Expand Down
10 changes: 5 additions & 5 deletions docs/features/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ without line numbers, so you can copy them to the clipboard:

(Cmd) history -s 1:3

`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:
`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:

(Cmd) alias create ls shell ls -aF
Alias 'ls' created
Expand All @@ -212,7 +212,7 @@ By default, the `history` command shows exactly what we typed:
1 alias create ls shell ls -aF
2 ls -d h*

There are two ways to modify the display so you can see what aliases and shortcuts were expanded to.
There are two ways to modify the display so you can see what aliases and macros were expanded to.
The first is to use `-x` or `--expanded`. These options show the expanded command instead of the
entered command:

Expand All @@ -229,5 +229,5 @@ option:
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 aliases, then the entered command is displayed with the number, and the
expanded command is displayed with the number followed by an `x`.
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`.
2 changes: 1 addition & 1 deletion docs/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- [Output Redirection and Pipes](redirection.md)
- [Scripting](scripting.md)
- [Settings](settings.md)
- [Shortcuts and Aliases](shortcuts_aliases.md)
- [Shortcuts, Aliases, and Macros](shortcuts_aliases_macros.md)
- [Startup Commands](startup_commands.md)
- [Table Creation](table_creation.md)
- [Transcripts](transcripts.md)
Expand Down
1 change: 1 addition & 0 deletions docs/features/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri
- **help_error**: the error that prints when no help information can be found
- **hidden_commands**: commands to exclude from the help menu and tab completion
- **last_result**: stores results from the last command run to enable usage 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 displaying wrapped output using a pager
- **pager_chop**: sets the pager command used by the `Cmd.ppaged()` method for displaying chopped/truncated output using a pager
Expand Down
8 changes: 4 additions & 4 deletions docs/features/os.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ See [Output Redirection and Pipes](./redirection.md#output-redirection-and-pipes

(Cmd) shell ls -al

If you use the default [Shortcuts](./shortcuts_aliases.md#shortcuts) defined in `cmd2` you'll get a
`!` shortcut for `shell`, which allows you to type:
If you use the default [Shortcuts](./shortcuts_aliases_macros.md#shortcuts) defined in `cmd2` you'll
get a `!` shortcut for `shell`, which allows you to type:

(Cmd) !ls -al

Expand Down Expand Up @@ -89,8 +89,8 @@ shell, and execute those commands before entering the command loop:

Documented commands (use 'help -v' for verbose/'help <topic>' for details):
===========================================================================
alias help ipy quit run_script shell
edit history py run_pyscript set shortcuts
alias help macro orate quit run_script set shortcuts
edit history mumble py run_pyscript say shell speak

(Cmd)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Shortcuts and Aliases
# Shortcuts, Aliases, and Macros

## Shortcuts

Expand Down Expand Up @@ -26,7 +26,7 @@ class App(Cmd):

Shortcuts need to be created by updating the `shortcuts` dictionary attribute prior to calling the `cmd2.Cmd` super class `__init__()` method. Moreover, that super class init method needs to be called after updating the `shortcuts` attribute This warning applies in general to many other attributes which are not settable at runtime.

Note: Command and alias names cannot start with a shortcut
Note: Command, alias, and macro names cannot start with a shortcut

## Aliases

Expand Down Expand Up @@ -57,4 +57,41 @@ Use `alias delete` to remove aliases

For more details run: `help alias delete`

Note: Aliases cannot have the same name as a command
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]{#macro} that expects two arguments:

macro create my[macro]{#macro} make[dinner]{#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]{#macro} beef broccoli ---> make[dinner]{#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}}`.

Since macros don't resolve until after you press `<Enter>`, their arguments tab complete as paths.
You can change this default behavior by overriding `Cmd.macro_arg_complete()` to implement custom
tab completion for macro arguments.

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
2 changes: 1 addition & 1 deletion docs/migrating/incompatibilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ and arguments on whitespace. We opted for this breaking change because while
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 [Tab Completion](../features/completion.md) and
[Shortcuts and Aliases](../features/shortcuts_aliases.md).
[Shortcuts, Aliases, and Macros](../features/shortcuts_aliases_macros.md).

If you really need this functionality in your app, you can add it back in by writing a
[Postparsing Hook](../features/hooks.md#postparsing-hooks).
Expand Down
4 changes: 2 additions & 2 deletions docs/migrating/why.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ and capabilities, without you having to do anything:
Before you do, you might consider the various ways `cmd2` has of
[Generatoring Output](../features/generating_output.md).
- Users can load script files, which contain a series of commands to be executed.
- Users can create [Shortcuts and Aliases](../features/shortcuts_aliases.md) to reduce the typing
required for repetitive commands.
- Users can create [Shortcuts, Aliases, and Macros](../features/shortcuts_aliases_macros.md) 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.
- [Clipboard Integration](../features/clipboard.md) allows you to save command output to the
operating system clipboard.
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ nav:
- features/redirection.md
- features/scripting.md
- features/settings.md
- features/shortcuts_aliases.md
- features/shortcuts_aliases_macros.md
- features/startup_commands.md
- features/table_creation.md
- features/transcripts.md
Expand Down
Loading
Loading