diff options
author | kotfu <kotfu@kotfu.net> | 2019-07-02 19:02:36 -0600 |
---|---|---|
committer | kotfu <kotfu@kotfu.net> | 2019-07-02 19:02:36 -0600 |
commit | 92ae130c38520b249eb7351cfb0da1ad67d3d3cf (patch) | |
tree | a403641a1a9412b19e26b52fae83635d812f9409 /docs/features | |
parent | 80950bfa4216ed20df5d63f1ebe63bac5b3746b4 (diff) | |
download | cmd2-git-92ae130c38520b249eb7351cfb0da1ad67d3d3cf.tar.gz |
Major overhaul of documentation structure for #709
Diffstat (limited to 'docs/features')
-rw-r--r-- | docs/features/argument_processing.rst | 320 | ||||
-rw-r--r-- | docs/features/generating_output.rst | 10 | ||||
-rw-r--r-- | docs/features/help.rst | 8 | ||||
-rw-r--r-- | docs/features/hooks.rst | 305 | ||||
-rw-r--r-- | docs/features/transcript.rst | 193 |
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() |