summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2017-03-11 10:26:34 -0500
committerTodd Leonhardt <todd.leonhardt@gmail.com>2017-03-11 10:26:34 -0500
commit31036ef97bba35763b2025c4ea6befd872b69276 (patch)
tree298377bc58042f11428b01883954391c4596a141
parentabe33f2813ecf7f2caaf6cf192394d7e56473563 (diff)
downloadcmd2-git-31036ef97bba35763b2025c4ea6befd872b69276.tar.gz
Added an example for how conditional control flow of a cmd2 application can be achieved via the py command and python scripts.
-rwxr-xr-xcmd2.py31
-rw-r--r--docs/freefeatures.rst7
-rwxr-xr-xexamples/python_scripting.py102
-rw-r--r--examples/script_conditional.py24
4 files changed, 163 insertions, 1 deletions
diff --git a/cmd2.py b/cmd2.py
index e4626625..5ad6bf40 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -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')