Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import traceback
import unittest
from code import InteractiveConsole
from collections import namedtuple
from optparse import make_option

import pyparsing
Expand Down Expand Up @@ -91,7 +92,7 @@
# 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


Expand Down Expand Up @@ -595,9 +596,16 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
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:
Expand Down Expand Up @@ -1823,6 +1831,27 @@ def tearDown(self):
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()
Expand Down
7 changes: 7 additions & 0 deletions docs/freefeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
==================

Expand Down
102 changes: 102 additions & 0 deletions examples/python_scripting.py
Original file line number Diff line number Diff line change
@@ -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()
24 changes: 24 additions & 0 deletions examples/script_conditional.py
Original file line number Diff line number Diff line change
@@ -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')