summaryrefslogtreecommitdiff
path: root/docs/features
diff options
context:
space:
mode:
Diffstat (limited to 'docs/features')
-rw-r--r--docs/features/argument_processing.rst320
-rw-r--r--docs/features/generating_output.rst10
-rw-r--r--docs/features/help.rst8
-rw-r--r--docs/features/hooks.rst305
-rw-r--r--docs/features/transcript.rst193
5 files changed, 836 insertions, 0 deletions
diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst
new file mode 100644
index 00000000..20ab7879
--- /dev/null
+++ b/docs/features/argument_processing.rst
@@ -0,0 +1,320 @@
+.. _decorators:
+
+Argument Processing
+===================
+
+``cmd2`` makes it easy to add sophisticated argument processing to your commands using the ``argparse`` python module.
+``cmd2`` handles the following for you:
+
+1. Parsing input and quoted strings like the Unix shell
+2. Parse the resulting argument list using an instance of ``argparse.ArgumentParser`` that you provide
+3. Passes the resulting ``argparse.Namespace`` object to your command function. The ``Namespace`` includes the
+ ``Statement`` object that was created when parsing the command line. It is stored in the ``__statement__``
+ attribute of the ``Namespace``.
+4. Adds the usage message from the argument parser to your command.
+5. Checks if the ``-h/--help`` option is present, and if so, display the help message for the command
+
+These features are all provided by the ``@with_argparser`` decorator which is importable from ``cmd2``.
+
+See the either the argprint_ or decorator_ example to learn more about how to use the various ``cmd2`` argument
+processing decorators in your ``cmd2`` applications.
+
+.. _argprint: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py
+.. _decorator: https://github.com/python-cmd2/cmd2/blob/master/examples/decorator_example.py
+
+
+Decorators provided by cmd2 for argument processing
+---------------------------------------------------
+
+``cmd2`` provides the following decorators for assisting with parsing arguments passed to commands:
+
+.. automethod:: cmd2.cmd2.with_argument_list
+ :noindex:
+.. automethod:: cmd2.cmd2.with_argparser
+ :noindex:
+.. automethod:: cmd2.cmd2.with_argparser_and_unknown_args
+ :noindex:
+
+All of these decorators accept an optional **preserve_quotes** argument which defaults to ``False``.
+Setting this argument to ``True`` is useful for cases where you are passing the arguments to another
+command which might have its own argument parsing.
+
+
+Using the argument parser decorator
+-----------------------------------
+
+For each command in the ``cmd2`` subclass which requires argument parsing,
+create a unique instance of ``argparse.ArgumentParser()`` which can parse the
+input appropriately for the command. Then decorate the command method with
+the ``@with_argparser`` decorator, passing the argument parser as the
+first parameter to the decorator. This changes the second argument to the command method, which will contain the results
+of ``ArgumentParser.parse_args()``.
+
+Here's what it looks like::
+
+ import argparse
+ from cmd2 import with_argparser
+
+ argparser = argparse.ArgumentParser()
+ argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
+ argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
+ argparser.add_argument('-r', '--repeat', type=int, help='output [n] times')
+ argparser.add_argument('word', nargs='?', help='word to say')
+
+ @with_argparser(argparser)
+ def do_speak(self, opts)
+ """Repeats what you tell me to."""
+ arg = opts.word
+ if opts.piglatin:
+ arg = '%s%say' % (arg[1:], arg[0])
+ if opts.shout:
+ arg = arg.upper()
+ repetitions = opts.repeat or 1
+ for i in range(min(repetitions, self.maxrepeats)):
+ self.poutput(arg)
+
+.. warning::
+
+ It is important that each command which uses the ``@with_argparser`` decorator be passed a unique instance of a
+ parser. This limitation is due to bugs in CPython prior to Python 3.7 which make it impossible to make a deep copy
+ of an instance of a ``argparse.ArgumentParser``.
+
+ See the table_display_ example for a work-around that demonstrates how to create a function which returns a unique
+ instance of the parser you want.
+
+
+.. note::
+
+ The ``@with_argparser`` decorator sets the ``prog`` variable in
+ the argument parser based on the name of the method it is decorating.
+ This will override anything you specify in ``prog`` variable when
+ creating the argument parser.
+
+.. _table_display: https://github.com/python-cmd2/cmd2/blob/master/examples/table_display.py
+
+
+Help Messages
+-------------
+
+By default, cmd2 uses the docstring of the command method when a user asks
+for help on the command. When you use the ``@with_argparser``
+decorator, the docstring for the ``do_*`` method is used to set the description for the ``argparse.ArgumentParser``.
+
+With this code::
+
+ import argparse
+ from cmd2 import with_argparser
+
+ argparser = argparse.ArgumentParser()
+ argparser.add_argument('tag', help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argparser(argparser)
+ def do_tag(self, args):
+ """create a html tag"""
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
+ self.stdout.write('\n')
+
+the ``help tag`` command displays:
+
+.. code-block:: none
+
+ usage: tag [-h] tag content [content ...]
+
+ create a html tag
+
+ positional arguments:
+ tag tag
+ content content to surround with tag
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
+If you would prefer you can set the ``description`` while instantiating the ``argparse.ArgumentParser`` and leave the
+docstring on your method empty::
+
+ import argparse
+ from cmd2 import with_argparser
+
+ argparser = argparse.ArgumentParser(description='create an html tag')
+ argparser.add_argument('tag', help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argparser(argparser)
+ def do_tag(self, args):
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
+ self.stdout.write('\n')
+
+Now when the user enters ``help tag`` they see:
+
+.. code-block:: none
+
+ usage: tag [-h] tag content [content ...]
+
+ create an html tag
+
+ positional arguments:
+ tag tag
+ content content to surround with tag
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
+To add additional text to the end of the generated help message, use the ``epilog`` variable::
+
+ import argparse
+ from cmd2 import with_argparser
+
+ argparser = argparse.ArgumentParser(description='create an html tag',
+ epilog='This command can not generate tags with no content, like <br/>.')
+ argparser.add_argument('tag', help='tag')
+ argparser.add_argument('content', nargs='+', help='content to surround with tag')
+ @with_argparser(argparser)
+ def do_tag(self, args):
+ self.stdout.write('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
+ self.stdout.write('\n')
+
+Which yields:
+
+.. code-block:: none
+
+ usage: tag [-h] tag content [content ...]
+
+ create an html tag
+
+ positional arguments:
+ tag tag
+ content content to surround with tag
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ This command can not generate tags with no content, like <br/>
+
+.. warning::
+
+ If a command **foo** is decorated with one of cmd2's argparse decorators, then **help_foo** will not
+ be invoked when ``help foo`` is called. The argparse_ module provides a rich API which can be used to
+ tweak every aspect of the displayed help and we encourage ``cmd2`` developers to utilize that.
+
+.. _argparse: https://docs.python.org/3/library/argparse.html
+
+
+Receiving an argument list
+--------------------------
+
+The default behavior of ``cmd2`` is to pass the user input directly to your
+``do_*`` methods as a string. The object passed to your method is actually a
+``Statement`` object, which has additional attributes that may be helpful,
+including ``arg_list`` and ``argv``::
+
+ class CmdLineApp(cmd2.Cmd):
+ """ Example cmd2 application. """
+
+ def do_say(self, statement):
+ # statement contains a string
+ self.poutput(statement)
+
+ def do_speak(self, statement):
+ # statement also has a list of arguments
+ # quoted arguments remain quoted
+ for arg in statement.arg_list:
+ self.poutput(arg)
+
+ def do_articulate(self, statement):
+ # statement.argv contains the command
+ # and the arguments, which have had quotes
+ # stripped
+ for arg in statement.argv:
+ self.poutput(arg)
+
+
+If you don't want to access the additional attributes on the string passed to
+you``do_*`` method you can still have ``cmd2`` apply shell parsing rules to the
+user input and pass you a list of arguments instead of a string. Apply the
+``@with_argument_list`` decorator to those methods that should receive an
+argument list instead of a string::
+
+ from cmd2 import with_argument_list
+
+ class CmdLineApp(cmd2.Cmd):
+ """ Example cmd2 application. """
+
+ def do_say(self, cmdline):
+ # cmdline contains a string
+ pass
+
+ @with_argument_list
+ def do_speak(self, arglist):
+ # arglist contains a list of arguments
+ pass
+
+
+Using the argument parser decorator and also receiving a list of unknown positional arguments
+---------------------------------------------------------------------------------------------
+
+If you want all unknown arguments to be passed to your command as a list of strings, then
+decorate the command method with the ``@with_argparser_and_unknown_args`` decorator.
+
+Here's what it looks like::
+
+ import argparse
+ from cmd2 import with_argparser_and_unknown_args
+
+ dir_parser = argparse.ArgumentParser()
+ dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line")
+
+ @with_argparser_and_unknown_args(dir_parser)
+ def do_dir(self, args, unknown):
+ """List contents of current directory."""
+ # No arguments for this command
+ if unknown:
+ self.perror("dir does not take any positional arguments:")
+ self.do_help('dir')
+ self.last_result = CommandResult('', 'Bad arguments')
+ return
+
+ # Get the contents as a list
+ contents = os.listdir(self.cwd)
+
+ ...
+
+Using custom argparse.Namespace with argument parser decorators
+---------------------------------------------------------------
+
+In some cases, it may be necessary to write custom ``argparse`` code that is dependent on state data of your
+application. To support this ability while still allowing use of the decorators, both ``@with_argparser`` and
+``@with_argparser_and_unknown_args`` have an optional argument called ``ns_provider``.
+
+``ns_provider`` is a Callable that accepts a ``cmd2.Cmd`` object as an argument and returns an ``argparse.Namespace``::
+
+ Callable[[cmd2.Cmd], argparse.Namespace]
+
+For example::
+
+ def settings_ns_provider(self) -> argparse.Namespace:
+ """Populate an argparse Namespace with current settings"""
+ ns = argparse.Namespace()
+ ns.app_settings = self.settings
+ return ns
+
+To use this function with the argparse decorators, do the following::
+
+ @with_argparser(my_parser, ns_provider=settings_ns_provider)
+
+The Namespace is passed by the decorators to the ``argparse`` parsing functions which gives your custom code access
+to the state data it needs for its parsing logic.
+
+Sub-commands
+------------
+
+Sub-commands are supported for commands using either the ``@with_argparser`` or
+``@with_argparser_and_unknown_args`` decorator. The syntax for supporting them is based on argparse sub-parsers.
+
+You may add multiple layers of sub-commands for your command. Cmd2 will automatically traverse and tab-complete
+sub-commands for all commands using argparse.
+
+See the subcommands_ and tab_autocompletion_ example to learn more about how to use sub-commands in your ``cmd2`` application.
+
+.. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py
+.. _tab_autocompletion: https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py
diff --git a/docs/features/generating_output.rst b/docs/features/generating_output.rst
new file mode 100644
index 00000000..a4a928cf
--- /dev/null
+++ b/docs/features/generating_output.rst
@@ -0,0 +1,10 @@
+Generating Output
+=================
+
+how to generate output
+
+poutput
+
+perror
+
+paging
diff --git a/docs/features/help.rst b/docs/features/help.rst
new file mode 100644
index 00000000..e5cc0451
--- /dev/null
+++ b/docs/features/help.rst
@@ -0,0 +1,8 @@
+Help
+====
+
+use the categorize() function to create help categories
+
+Use ``help_method()`` to custom roll your own help messages.
+
+See :ref:`features/argument_processing:Help Messages`
diff --git a/docs/features/hooks.rst b/docs/features/hooks.rst
new file mode 100644
index 00000000..5db97fe5
--- /dev/null
+++ b/docs/features/hooks.rst
@@ -0,0 +1,305 @@
+.. cmd2 documentation for application and command lifecycle and the available hooks
+
+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 of and arguments to ``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:
+
+#. Output the prompt
+#. Accept user input
+#. Parse user input into `Statement` object
+#. Call methods registered with `register_postparsing_hook()`
+#. Redirect output, if user asked for it and it's allowed
+#. Start timer
+#. Call methods registered with `register_precmd_hook()`
+#. Call `precmd()` - for backwards compatibility with ``cmd.Cmd``
+#. Add statement to history
+#. Call `do_command` method
+#. Call methods registered with `register_postcmd_hook()`
+#. Call `postcmd(stop, statement)` - for backwards compatibility with ``cmd.Cmd``
+#. Stop timer and display the elapsed time
+#. Stop redirecting output if it was redirected
+#. Call methods registered with `register_cmdfinalization_hook()`
+
+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 data
+
+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 a ``PostcommandData`` object
+with the stop attribute set to ``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. Likewise, if ``self.postcmd()`` returns ``True``, the command finalization hooks
+will be called before the application terminates.
+
+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.
diff --git a/docs/features/transcript.rst b/docs/features/transcript.rst
new file mode 100644
index 00000000..089ab704
--- /dev/null
+++ b/docs/features/transcript.rst
@@ -0,0 +1,193 @@
+========================
+Transcript based testing
+========================
+
+A transcript is both the input and output of a successful session of a
+``cmd2``-based app which is saved to a text file. With no extra work on your
+part, your app can play back these transcripts as a unit test. Transcripts can
+contain regular expressions, which provide the flexibility to match responses
+from commands that produce dynamic or variable output.
+
+.. highlight:: none
+
+Creating a transcript
+=====================
+
+Automatically from history
+--------------------------
+A transcript can automatically generated based upon commands previously executed in the *history* using ``history -t``::
+
+ (Cmd) help
+ ...
+ (Cmd) help history
+ ...
+ (Cmd) history 1:2 -t transcript.txt
+ 2 commands and outputs saved to transcript file 'transcript.txt'
+
+This is by far the easiest way to generate a transcript.
+
+.. warning::
+
+ Make sure you use the **poutput()** method in your ``cmd2`` application for generating command output. This method
+ of the ``cmd2.Cmd`` class ensure that output is properly redirected when redirecting to a file, piping to a shell
+ command, and when generating a transcript.
+
+Automatically from a script file
+--------------------------------
+A transcript can also be automatically generated from a script file using ``run_script -t``::
+
+ (Cmd) run_script scripts/script.txt -t transcript.txt
+ 2 commands and their outputs saved to transcript file 'transcript.txt'
+ (Cmd)
+
+This is a particularly attractive option for automatically regenerating transcripts for regression testing as your ``cmd2``
+application changes.
+
+Manually
+--------
+Here's a transcript created from ``python examples/example.py``::
+
+ (Cmd) say -r 3 Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ (Cmd) mumble maybe we could go to lunch
+ like maybe we ... could go to hmmm lunch
+ (Cmd) mumble maybe we could go to lunch
+ well maybe we could like go to er lunch right?
+
+This transcript has three commands: they are on the lines that begin with the
+prompt. The first command looks like this::
+
+ (Cmd) say -r 3 Goodnight, Gracie
+
+Following each command is the output generated by that command.
+
+The transcript ignores all lines in the file until it reaches the first line
+that begins with the prompt. You can take advantage of this by using the first
+lines of the transcript as comments::
+
+ # Lines at the beginning of the transcript that do not
+ ; start with the prompt i.e. '(Cmd) ' are ignored.
+ /* You can use them for comments. */
+
+ All six of these lines before the first prompt are treated as comments.
+
+ (Cmd) say -r 3 Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ Goodnight, Gracie
+ (Cmd) mumble maybe we could go to lunch
+ like maybe we ... could go to hmmm lunch
+ (Cmd) mumble maybe we could go to lunch
+ maybe we could like go to er lunch right?
+
+In this example I've used several different commenting styles, and even bare
+text. It doesn't matter what you put on those beginning lines. Everything before::
+
+ (Cmd) say -r 3 Goodnight, Gracie
+
+will be ignored.
+
+
+Regular Expressions
+===================
+
+If we used the above transcript as-is, it would likely fail. As you can see,
+the ``mumble`` command doesn't always return the same thing: it inserts random
+words into the input.
+
+Regular expressions can be included in the response portion of a transcript,
+and are surrounded by slashes::
+
+ (Cmd) mumble maybe we could go to lunch
+ /.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
+ (Cmd) mumble maybe we could go to lunch
+ /.*\bmaybe\b.*\bcould\b.*\blunch\b.*/
+
+Without creating a tutorial on regular expressions, this one matches anything
+that has the words ``maybe``, ``could``, and ``lunch`` in that order. It doesn't
+ensure that ``we`` or ``go`` or ``to`` appear in the output, but it does work if
+mumble happens to add words to the beginning or the end of the output.
+
+Since the output could be multiple lines long, ``cmd2`` uses multiline regular
+expression matching, and also uses the ``DOTALL`` flag. These two flags subtly
+change the behavior of commonly used special characters like ``.``, ``^`` and
+``$``, so you may want to double check the `Python regular expression
+documentation <https://docs.python.org/3/library/re.html>`_.
+
+If your output has slashes in it, you will need to escape those slashes so the
+stuff between them is not interpred as a regular expression. In this transcript::
+
+ (Cmd) say cd /usr/local/lib/python3.6/site-packages
+ /usr/local/lib/python3.6/site-packages
+
+the output contains slashes. The text between the first slash and the second
+slash, will be interpreted as a regular expression, and those two slashes will
+not be included in the comparison. When replayed, this transcript would
+therefore fail. To fix it, we could either write a regular expression to match
+the path instead of specifying it verbatim, or we can escape the slashes::
+
+ (Cmd) say cd /usr/local/lib/python3.6/site-packages
+ \/usr\/local\/lib\/python3.6\/site-packages
+
+.. warning::
+
+ Be aware of trailing spaces and newlines. Your commands might output
+ trailing spaces which are impossible to see. Instead of leaving them
+ invisible, you can add a regular expression to match them, so that you can
+ see where they are when you look at the transcript::
+
+ (Cmd) set prompt
+ prompt: (Cmd)/ /
+
+ Some terminal emulators strip trailing space when you copy text from them.
+ This could make the actual data generated by your app different than the
+ text you pasted into the transcript, and it might not be readily obvious why
+ the transcript is not passing. Consider using :ref:`output_redirection` to
+ the clipboard or to a file to ensure you accurately capture the output of
+ your command.
+
+ If you aren't using regular expressions, make sure the newlines at the end
+ of your transcript exactly match the output of your commands. A common cause
+ of a failing transcript is an extra or missing newline.
+
+ If you are using regular expressions, be aware that depending on how you
+ write your regex, the newlines after the regex may or may not matter.
+ ``\Z`` matches *after* the newline at the end of the string, whereas
+ ``$`` matches the end of the string *or* just before a newline.
+
+
+Running a transcript
+====================
+
+Once you have created a transcript, it's easy to have your application play it
+back and check the output. From within the ``examples/`` directory::
+
+ $ python example.py --test transcript_regex.txt
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 0.013s
+
+ OK
+
+The output will look familiar if you use ``unittest``, because that's exactly
+what happens. Each command in the transcript is run, and we ``assert`` the
+output matches the expected result from the transcript.
+
+.. note::
+
+ If you have set ``allow_cli_args`` to False in order to disable parsing of
+ command line arguments at invocation, then the use of ``-t`` or ``--test``
+ to run transcript testing is automatically disabled. In this case, you can
+ alternatively provide a value for the optional ``transcript_files`` when
+ constructing the instance of your ``cmd2.Cmd`` derived class in order to
+ cause a transcript test to run::
+
+ from cmd2 import Cmd
+ class App(Cmd):
+ # customized attributes and methods here
+
+ if __name__ == '__main__':
+ app = App(transcript_files=['exampleSession.txt'])
+ app.cmdloop()