cmd2 Application Lifecycle and Hooks

The typical way of starting a cmd2 application is as follows:

import cmd2
class App(cmd2.Cmd):
    # customized attributes and methods here

if __name__ == '__main__':
    app = App()
    app.cmdloop()

There are several pre-existing methods and attributes which you can tweak to control the overall behavior of your application before, during, and after the command processing loop.

Application Lifecycle Hooks

You can register methods to be called at the beginning of the command loop:

class App(cmd2.Cmd):
    def __init__(self, *args, *kwargs):
        super().__init__(*args, **kwargs)
        self.register_preloop_hook(self.myhookmethod)

    def myhookmethod(self):
        self.poutput("before the loop begins")

To retain backwards compatibility with cmd.Cmd, after all registered preloop hooks have been called, the preloop() method is called.

A similar approach allows you to register functions to be called after the command loop has finished:

class App(cmd2.Cmd):
    def __init__(self, *args, *kwargs):
        super().__init__(*args, **kwargs)
        self.register_postloop_hook(self.myhookmethod)

    def myhookmethod(self):
        self.poutput("before the loop begins")

To retain backwards compatibility with cmd.Cmd, after all registered postloop hooks have been called, the postloop() method is called.

Preloop and postloop hook methods are not passed any parameters and any return value is ignored.

Application Lifecycle Attributes

There are numerous attributes (member variables of the cmd2.Cmd) which have a significant effect on the application behavior upon entering or during the main loop. A partial list of some of the more important ones is presented here:

  • intro: str - if provided this serves as the intro banner printed once at start of application, after preloop runs
  • allow_cli_args: bool - if True (default), then searches for -t or –test at command line to invoke transcript testing mode instead of a normal main loop and also processes any commands provided as arguments on the command line just prior to entering the main loop
  • echo: bool - if True, then the command line entered is echoed to the screen (most useful when running scripts)
  • prompt: str - sets the prompt which is displayed, can be dynamically changed based on application state and/or command results

Command Processing Loop

When you call .cmdloop(), the following sequence of events are repeated until the application exits:

  1. Output the prompt
  2. Accept user input
  3. Call preparse() - for backwards compatibility with prior releases of cmd2, now deprecated
  4. Parse user input into Statement object
  5. Call methods registered with register_postparsing_hook()
  6. Call postparsing_precmd() - for backwards compatibility with prior releases of cmd2, now deprecated
  7. Redirect output, if user asked for it and it’s allowed
  8. Start timer
  9. Call methods registered with register_precmd_hook()
  10. Call precmd() - for backwards compatibility with cmd.Cmd
  11. Add statement to history
  12. Call do_command method
  13. Call methods registered with register_postcmd_hook()
  14. Call postcmd(stop, statement) - for backwards compatibility with cmd.Cmd
  15. Stop timer and display the elapsed time
  16. Stop redirecting output if it was redirected
  17. Call methods registered with register_cmdfinalization_hook()
  18. Call postparsing_postcmd() - for backwards compatibility - deprecated

By registering hook methods, steps 4, 8, 12, and 16 allow you to run code during, and control the flow of the command processing loop. Be aware that plugins also utilize these hooks, so there may be code running that is not part of your application. Methods registered for a hook are called in the order they were registered. You can register a function more than once, and it will be called each time it was registered.

Postparsing, precommand, and postcommand hook methods share some common ways to influence the command processing loop.

If a hook raises a cmd2.EmptyStatement exception: - no more hooks (except command finalization hooks) of any kind will be called - if the command has not yet been executed, it will not be executed - no error message will be displayed to the user

If a hook raises any other exception: - no more hooks (except command finalization hooks) of any kind will be called - if the command has not yet been executed, it will not be executed - the exception message will be displayed for the user.

Specific types of hook methods have additional options as described below.

Postparsing Hooks

Postparsing hooks are called after the user input has been parsed but before execution of the command. These hooks can be used to:

  • modify the user input
  • run code before every command executes
  • cancel execution of the current command
  • exit the application

When postparsing hooks are called, output has not been redirected, nor has the timer for command execution been started.

To define and register a postparsing hook, do the following:

class App(cmd2.Cmd):
    def __init__(self, *args, *kwargs):
        super().__init__(*args, **kwargs)
        self.register_postparsing_hook(self.myhookmethod)

    def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        # the statement object created from the user input
        # is available as params.statement
        return params

register_postparsing_hook() checks the method signature of the passed callable, and raises a TypeError if it has the wrong number of parameters. It will also raise a TypeError if the passed parameter and return value are not annotated as PostparsingData.

The hook method will be passed one parameter, a PostparsingData object which we will refer to as params. params contains two attributes. params.statement is a Statement object which describes the parsed user input. There are many useful attributes in the Statement object, including .raw which contains exactly what the user typed. params.stop is set to False by default.

The hook method must return a PostparsingData object, and it is very convenient to just return the object passed into the hook method. The hook method may modify the attributes of the object to influece the behavior of the application. If params.stop is set to true, a fatal failure is triggered prior to execution of the command, and the application exits.

To modify the user input, you create a new Statement object and return it in params.statement. Don’t try and directly modify the contents of a Statement object, there be dragons. Instead, use the various attributes in a Statement object to construct a new string, and then parse that string to create a new Statement object.

cmd2.Cmd() uses an instance of cmd2.StatementParser to parse user input. This instance has been configured with the proper command terminators, multiline commands, and other parsing related settings. This instance is available as the self.statement_parser attribute. Here’s a simple example which shows the proper technique:

def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
    if not '|' in params.statement.raw:
        newinput = params.statement.raw + ' | less'
        params.statement = self.statement_parser.parse(newinput)
    return params

If a postparsing hook returns a PostparsingData object with the stop attribute set to True:

  • no more hooks of any kind (except command finalization hooks) will be called
  • the command will not be executed
  • no error message will be displayed to the user
  • the application will exit

Precommand Hooks

Precommand hooks can modify the user input, but can not request the application terminate. If your hook needs to be able to exit the application, you should implement it as a postparsing hook.

Once output is redirected and the timer started, all the hooks registered with register_precmd_hook() are called. Here’s how to do it:

class App(cmd2.Cmd):
    def __init__(self, *args, *kwargs):
        super().__init__(*args, **kwargs)
        self.register_precmd_hook(self.myhookmethod)

    def myhookmethod(self, data: cmd2.plugin.PrecommandData) -> cmd2.plugin.PrecommandData:
        # the statement object created from the user input
        # is available as data.statement
        return data

register_precmd_hook() checks the method signature of the passed callable, and raises a TypeError if it has the wrong number of parameters. It will also raise a TypeError if the parameters and return value are not annotated as PrecommandData.

You may choose to modify the user input by creating a new Statement with different properties (see above). If you do so, assign your new Statement object to data.statement.

The precommand hook must return a PrecommandData object. You don’t have to create this object from scratch, you can just return the one passed into the hook.

After all registered precommand hooks have been called, self.precmd(statement) will be called. To retain full backward compatibility with cmd.Cmd, this method is passed a Statement, not a PrecommandData object.

Postcommand Hooks

Once the command method has returned (i.e. the do_command(self, statement) method has been called and returns, all postcommand hooks are called. If output was redirected by the user, it is still redirected, and the command timer is still running.

Here’s how to define and register a postcommand hook:

class App(cmd2.Cmd):
    def __init__(self, *args, *kwargs):
        super().__init__(*args, **kwargs)
        self.register_postcmd_hook(self.myhookmethod)

    def myhookmethod(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.PostcommandData:
        return stop

Your hook will be passed a PostcommandData object, which has a statement attribute that describes the command which was executed. If your postcommand hook method gets called, you are guaranteed that the command method was called, and that it didn’t raise an exception.

If any postcommand hook raises an exception, the exception will be displayed to the user, and no further postcommand hook methods will be called. Command finalization hooks, if any, will be called.

After all registered postcommand hooks have been called, self.postcmd(statement) will be called to retain full backward compatibility with cmd.Cmd.

If any postcommand hook (registered or self.postcmd()) returns True, subsequent postcommand hooks will still be called, as will the command finalization hooks, but once those hooks have all been called, the application will terminate.

Any postcommand hook can change the value of the stop parameter before returning it, and the modified value will be passed to the next postcommand hook. The value returned by the final postcommand hook will be passed to the command finalization hooks, which may further modify the value. If your hook blindly returns False, a prior hook’s requst to exit the application will not be honored. It’s best to return the value you were passed unless you have a compelling reason to do otherwise.

Command Finalization Hooks

Command finalization hooks are called even if one of the other types of hooks or the command method raise an exception. Here’s how to create and register a command finalization hook:

class App(cmd2.Cmd):
    def __init__(self, *args, *kwargs):
        super().__init__(*args, **kwargs)
        self.register_cmdfinalization_hook(self.myhookmethod)

    def myhookmethod(self, stop, statement):
        return stop

Command Finalization hooks must check whether the statement object is None. There are certain circumstances where these hooks may be called before the statement has been parsed, so you can’t always rely on having a statement.

If any prior postparsing or precommand hook has requested the application to terminate, the value of the stop parameter passed to the first command finalization hook will be True. Any command finalization hook can change the value of the stop parameter before returning it, and the modified value will be passed to the next command finalization hook. The value returned by the final command finalization hook will determine whether the application terminates or not.

This approach to command finalization hooks can be powerful, but it can also cause problems. If your hook blindly returns False, a prior hook’s requst to exit the application will not be honored. It’s best to return the value you were passed unless you have a compelling reason to do otherwise.

If any command finalization hook raises an exception, no more command finalization hooks will be called. If the last hook to return a value returned True, then the exception will be rendered, and the application will terminate.

Deprecated Command Processing Hooks

Inside the main loop, every time the user hits <Enter> the line is processed by the onecmd_plus_hooks method.

Cmd.onecmd_plus_hooks(line: str) → bool

Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.

Parameters:line – line of text read from input
Returns:True if cmdloop() should exit, False otherwise

As the onecmd_plus_hooks name implies, there are a number of hook methods that can be defined in order to inject application-specific behavior at various points during the processing of a line of text entered by the user. cmd2 increases the 2 hooks provided by cmd (precmd and postcmd) to 6 for greater flexibility. Here are the various hook methods, presented in chronological order starting with the ones called earliest in the process.

Cmd.preparse(raw: str) → str

Hook method executed before user input is parsed.

WARNING: If it’s a multiline command, preparse() may not get all the user input. _complete_statement() really does two things: a) parse the user input, and b) accept more input in case it’s a multiline command the passed string doesn’t have a terminator. preparse() is currently called before we know whether it’s a multiline command, and before we know whether the user input includes a termination character.

If you want a reliable pre parsing hook method, register a postparsing hook, modify the user input, and then reparse it.

Parameters:raw – raw command line input :return: potentially modified raw command line input
Returns:a potentially modified version of the raw input string
Cmd.postparsing_precmd(statement: cmd2.parsing.Statement) → Tuple[bool, cmd2.parsing.Statement]

This runs after parsing the command-line, but before anything else; even before adding cmd to history.

NOTE: This runs before precmd() and prior to any potential output redirection or piping.

If you wish to fatally fail this command and exit the application entirely, set stop = True.

If you wish to just fail this command you can do so by raising an exception:

  • raise EmptyStatement - will silently fail and do nothing
  • raise <AnyOtherException> - will fail and print an error message
Parameters:statement
  • the parsed command-line statement as a Statement object
Returns:(bool, statement) - (stop, statement) containing a potentially modified version of the statement object
Cmd.postparsing_postcmd(stop: bool) → bool

This runs after everything else, including after postcmd().

It even runs when an empty line is entered. Thus, if you need to do something like update the prompt due to notifications from a background thread, then this is the method you want to override to do it.

Parameters:stop – bool - True implies the entire application should exit.
Returns:bool - True implies the entire application should exit.