diff options
-rwxr-xr-x | cmd2.py | 31 | ||||
-rw-r--r-- | docs/freefeatures.rst | 7 | ||||
-rwxr-xr-x | examples/python_scripting.py | 102 | ||||
-rw-r--r-- | examples/script_conditional.py | 24 |
4 files changed, 163 insertions, 1 deletions
@@ -41,6 +41,7 @@ import tempfile import traceback import unittest from code import InteractiveConsole +from collections import namedtuple from optparse import make_option import pyparsing @@ -91,7 +92,7 @@ POSIX_SHLEX = False # Strip outer quotes for convenience if POSIX_SHLEX = False STRIP_QUOTES_FOR_NON_POSIX = True -# For option commandsm, pass a list of argument strings instead of a single argument string to the do_* methods +# For option commands, pass a list of argument strings instead of a single argument string to the do_* methods USE_ARG_LIST = False @@ -595,9 +596,16 @@ class Cmd(cmd.Cmd): self._temp_filename = None self._transcript_files = transcript_files + # Used to enable the ability for a Python script to quit the application self._should_quit = False + + # True if running inside a Python script or interactive console, False otherwise self._in_py = False + # 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. + self._last_result = None + def poutput(self, msg): """Convenient shortcut for self.stdout.write(); adds newline if necessary.""" if msg: @@ -1823,6 +1831,27 @@ class Cmd2TestCase(unittest.TestCase): self.outputTrap.tearDown() +#noinspection PyClassHasNoInit +class CmdResult(namedtuple('CmdResult', ['out', 'err', 'war'])): + """Derive a class to store results from a named tuple so we can tweak dunder methods for convenience. + + This is provided as a convenience and an example for one possible way for end users to store results in + the self._last_result attribute of cmd2.Cmd class instances. See the "python_scripting.py" example for how it can + be used to enable conditional control flow. + + Named tuple attribues + --------------------- + out - this is intended to store normal output data from the command and can be of any type that makes sense + err: str - this is intended to store an error message and it being non-empty indicates there was an error + war: str - this is intended to store a warning message which isn't quite an error, but of note + + NOTE: Named tuples are immutable. So the contents are there for access, not for modification. + """ + def __bool__(self): + """If err is an empty string, treat the result as a success; otherwise treat it as a failure.""" + return not self.err + + if __name__ == '__main__': # If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality. app = Cmd() diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst index a767ac8c..a036973d 100644 --- a/docs/freefeatures.rst +++ b/docs/freefeatures.rst @@ -154,6 +154,13 @@ and any variables created or changed will persist for the life of the applicatio (Cmd) py print(x) 5 +The ``py`` command also allows you to run Python scripts via ``py run('myscript.py')``. +This provides a more complicated and more powerful scripting capability than that +provided by the simple text file scripts discussed in :ref:`scripts`. Python scripts can include +conditional control flow logic. See the **python_scripting.py** ``cmd2`` application and +the **script_conditional.py** script in the ``examples`` source code directory for an +example of how to achieve this in your own applications. + IPython (optional) ================== diff --git a/examples/python_scripting.py b/examples/python_scripting.py new file mode 100755 index 00000000..bf3a5222 --- /dev/null +++ b/examples/python_scripting.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# coding=utf-8 +"""A sample application for how Python scripting can provide conditional control flow of a cmd2 application. + +cmd2's built-in scripting capability which can be invoked via the "@" shortcut or "load" command and uses basic ASCII +text scripts is very easy to use. Moreover, the trivial syntax of the script files where there is one command per line +and the line is exactly what the user would type inside the application makes it so non-technical end users can quickly +learn to create scripts. + +However, there comes a time when technical end users want more capability and power. In particular it is common that +users will want to create a script with conditional control flow - where the next command run will depend on the results +from the previous command. This is where the ability to run Python scripts inside a cmd2 application via the py command +and the "py run('myscript.py')" syntax comes into play. + +This application and the "script_conditional.py" script serve as an example for one way in which this can be done. +""" +import os + +from cmd2 import Cmd, options, make_option, CmdResult, set_use_arg_list + +# For option commands, pass a list of argument strings instead of a single argument string to the do_* methods +set_use_arg_list(True) + + +class CmdLineApp(Cmd): + """ Example cmd2 application to showcase conditional control flow in Python scripting within cmd2 aps. """ + + def __init__(self): + Cmd.__init__(self) + self._set_prompt() + + def _set_prompt(self): + """Set prompt so it displays the current working directory.""" + self.prompt = '{!r} $ '.format(os.getcwd()) + + def postcmd(self, stop, line): + """Override this so prompt always displays cwd.""" + self._set_prompt() + return stop + + # noinspection PyUnusedLocal + @options([], arg_desc='<new_dir>') + def do_cd(self, arg, opts=None): + """Change directory.""" + # Expect 1 argument, the directory to change to + if not arg or len(arg) != 1: + self.perror("cd requires exactly 1 argument:", traceback_war=False) + self.do_help('cd') + self._last_result = CmdResult('', 'Bad arguments', '') + return + + # Convert relative paths to absolute paths + path = os.path.abspath(os.path.expanduser(arg[0])) + + # Make sure the directory exists, is a directory, and we have read access + out = '' + err = '' + war = '' + if not os.path.isdir(path): + err = '{!r} is not a directory'.format(path) + elif not os.access(path, os.R_OK): + err = 'You do not have read access to {!r}'.format(path) + else: + try: + os.chdir(path) + except Exception as ex: + err = '{}'.format(ex) + else: + out = 'Successfully changed directory to {!r}\n'.format(path) + self.stdout.write(out) + + if err: + self.perror(err, traceback_war=False) + self._last_result = CmdResult(out, err, war) + + @options([make_option('-l', '--long', action="store_true", help="display in long format with one item per line")], + arg_desc='') + def do_dir(self, arg, opts=None): + """List contents of current directory.""" + # No arguments for this command + if arg: + self.perror("dir does not take any arguments:", traceback_war=False) + self.do_help('dir') + self._last_result = CmdResult('', 'Bad arguments', '') + return + + # Get the contents as a list + contents = os.listdir(os.getcwd()) + + fmt = '{} ' + if opts.long: + fmt = '{}\n' + for f in contents: + self.stdout.write(fmt.format(f)) + self.stdout.write('\n') + + self._last_result = CmdResult(contents, '', '') + + +if __name__ == '__main__': + c = CmdLineApp() + c.cmdloop() diff --git a/examples/script_conditional.py b/examples/script_conditional.py new file mode 100644 index 00000000..cbfb0494 --- /dev/null +++ b/examples/script_conditional.py @@ -0,0 +1,24 @@ +# coding=utf-8 +""" +This is a Python script intended to be used with the "python_scripting.py" cmd2 example applicaiton. + +To run it you should do the following: + ./python_scripting.py + py run('script_conditional.py') + +Note: The "cmd" function is defined within the cmd2 embedded Python environment and in there "self" is your cmd2 +application instance. +""" + +# Try to change to a non-existent directory +cmd('cd foobar') + +# Conditionally do something based on the results of the last command +if self._last_result: + print('Contents of foobar directory:') + cmd('dir') +else: + # Change to parent directory + cmd('cd ..') + print('Contents of parent directory:') + cmd('dir') |