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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Once you have cmd2 cloned, before you start any cmd2 application, you first need

```bash
# Install cmd2 prerequisites
pip install -U six pyparsing
pip install -U six pyparsing pyperclip
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! I forgot to update this after adding a dependency on pyperclip


# Install prerequisites for running cmd2 unit tests
pip install -U pytest mock
Expand Down
57 changes: 45 additions & 12 deletions cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,44 @@ def onecmd_plus_hooks(self, line):
finally:
return self.postparsing_postcmd(stop)

def runcmds_plus_hooks(self, cmds):
"""Convenience method to run multiple commands by onecmd_plus_hooks.

This method adds the given cmds to the command queue and processes the
queue until completion or an error causes it to abort. Scripts that are
loaded will have their commands added to the queue. Scripts may even
load other scripts recursively. This means, however, that you should not
use this method if there is a running cmdloop or some other event-loop.
This method is only intended to be used in "one-off" scenarios.

NOTE: You may need this method even if you only have one command. If
that command is a load, then you will need this command to fully process
all the subsequent commands that are loaded from the script file. This
is an improvement over onecmd_plus_hooks, which expects to be used
inside of a command loop which does the processing of loaded commands.

Example: cmd_obj.runcmds_plus_hooks(['load myscript.txt'])

:param cmds: list - Command strings suitable for onecmd_plus_hooks.
:return: bool - True implies the entire application should exit.

"""
stop = False
self.cmdqueue = list(cmds) + self.cmdqueue
try:
while self.cmdqueue and not stop:
stop = self.onecmd_plus_hooks(self.cmdqueue.pop(0))
finally:
# Clear out the command queue and script directory stack, just in
# case we hit an error and they were not completed.
self.cmdqueue = []
self._script_dir = []
# NOTE: placing this return here inside the finally block will
# swallow exceptions. This is consistent with what is done in
# onecmd_plus_hooks and _cmdloop, although it may not be
# necessary/desired here.
return stop

def _complete_statement(self, line):
"""Keep accepting lines of input until the command is complete."""
if not line or (not pyparsing.Or(self.commentGrammars).setParseAction(lambda x: '').transformString(line)):
Expand Down Expand Up @@ -1775,18 +1813,13 @@ def do_load(self, file_path):
return

try:
# Specify file encoding in Python 3, but Python 2 doesn't allow that argument to open()
if six.PY3:
# Add all commands in the script to the command queue
with open(expanded_path, encoding='utf-8') as target:
self.cmdqueue.extend(target.read().splitlines())
else:
# Add all commands in the script to the command queue
with open(expanded_path) as target:
self.cmdqueue.extend(target.read().splitlines())

# Append in an "end of script (eos)" command to cleanup the self._script_dir list
self.cmdqueue.append('eos')
# Read all lines of the script and insert into the head of the
# command queue. Add an "end of script (eos)" command to cleanup the
# self._script_dir list when done. Specify file encoding in Python
# 3, but Python 2 doesn't allow that argument to open().
kwargs = {'encoding' : 'utf-8'} if six.PY3 else {}
with open(expanded_path, **kwargs) as target:
self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
except IOError as e:
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
return
Expand Down
4 changes: 4 additions & 0 deletions tests/scripts/nested.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
_relative_load precmds.txt
help
shortcuts
_relative_load postcmds.txt
2 changes: 2 additions & 0 deletions tests/scripts/postcmds.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set abbrev off
set colors off
2 changes: 2 additions & 0 deletions tests/scripts/precmds.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set abbrev on
set colors on
55 changes: 55 additions & 0 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,61 @@ def test_load_with_utf8_file(base_app, capsys, request):
assert base_app._current_script_dir == sdir


def test_load_nested_loads(base_app, request):
# Verify that loading a script with nested load commands works correctly,
# and loads the nested script commands in the correct order. The recursive
# loads don't happen all at once, but as the commands are interpreted. So,
# we will need to drain the cmdqueue and inspect the stdout to see if all
# steps were executed in the expected order.
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'scripts', 'nested.txt')
assert base_app.cmdqueue == []

# Load the top level script and then run the command queue until all
# commands have been exhausted.
initial_load = 'load ' + filename
run_cmd(base_app, initial_load)
while base_app.cmdqueue:
base_app.onecmd_plus_hooks(base_app.cmdqueue.pop(0))

# Check that the right commands were executed.
expected = """
%s
_relative_load precmds.txt
set abbrev on
set colors on
help
shortcuts
_relative_load postcmds.txt
set abbrev off
set colors off""" % initial_load
assert run_cmd(base_app, 'history -s') == normalize(expected)


def test_base_runcmds_plus_hooks(base_app, request):
# Make sure that runcmds_plus_hooks works as intended. I.E. to run multiple
# commands and process any commands added, by them, to the command queue.
test_dir = os.path.dirname(request.module.__file__)
prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt')
postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt')
assert base_app.cmdqueue == []

base_app.runcmds_plus_hooks(['load ' + prefilepath,
'help',
'shortcuts',
'load ' + postfilepath])
expected = """
load %s
set abbrev on
set colors on
help
shortcuts
load %s
set abbrev off
set colors off""" % (prefilepath, postfilepath)
assert run_cmd(base_app, 'history -s') == normalize(expected)


def test_base_relative_load(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
filename = os.path.join(test_dir, 'script.txt')
Expand Down