Skip to content

Commit 6fa589b

Browse files
committed
Provide method to run multiple commands w/o a cmdloop.
runcmds_plus_hooks can accept multiple commands process the command queue to deal with subsequent commands loaded from scripts without requiring a command loop. This better supports a one-off batch processing scenario. Also fixed the insertion order of commands placed in the command queue by load and _relative_load so that script commands are run in the expected order. Minor tweak to setup instructions in CONTRIBUTING.md to include pyperclip in prerequisites.
1 parent 6d711bc commit 6fa589b

File tree

6 files changed

+109
-13
lines changed

6 files changed

+109
-13
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ Once you have cmd2 cloned, before you start any cmd2 application, you first need
192192

193193
```bash
194194
# Install cmd2 prerequisites
195-
pip install -U six pyparsing
195+
pip install -U six pyparsing pyperclip
196196

197197
# Install prerequisites for running cmd2 unit tests
198198
pip install -U pytest mock

cmd2.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,44 @@ def onecmd_plus_hooks(self, line):
789789
finally:
790790
return self.postparsing_postcmd(stop)
791791

792+
def runcmds_plus_hooks(self, cmds):
793+
"""Convenience method to run multiple commands by onecmd_plus_hooks.
794+
795+
This method adds the given cmds to the command queue and processes the
796+
queue until completion or an error causes it to abort. Scripts that are
797+
loaded will have their commands added to the queue. Scripts may even
798+
load other scripts recursively. This means, however, that you should not
799+
use this method if there is a running cmdloop or some other event-loop.
800+
This method is only intended to be used in "one-off" scenarios.
801+
802+
NOTE: You may need this method even if you only have one command. If
803+
that command is a load, then you will need this command to fully process
804+
all the subsequent commands that are loaded from the script file. This
805+
is an improvement over onecmd_plus_hooks, which expects to be used
806+
inside of a command loop which does the processing of loaded commands.
807+
808+
Example: cmd_obj.runcmds_plus_hooks(['load myscript.txt'])
809+
810+
:param cmds: list - Command strings suitable for onecmd_plus_hooks.
811+
:return: bool - True implies the entire application should exit.
812+
813+
"""
814+
stop = False
815+
self.cmdqueue = list(cmds) + self.cmdqueue
816+
try:
817+
while self.cmdqueue and not stop:
818+
stop = self.onecmd_plus_hooks(self.cmdqueue.pop(0))
819+
finally:
820+
# Clear out the command queue and script directory stack, just in
821+
# case we hit an error and they were not completed.
822+
self.cmdqueue = []
823+
self._script_dir = []
824+
# NOTE: placing this return here inside the finally block will
825+
# swallow exceptions. This is consistent with what is done in
826+
# onecmd_plus_hooks and _cmdloop, although it may not be
827+
# necessary/desired here.
828+
return stop
829+
792830
def _complete_statement(self, line):
793831
"""Keep accepting lines of input until the command is complete."""
794832
if not line or (not pyparsing.Or(self.commentGrammars).setParseAction(lambda x: '').transformString(line)):
@@ -1775,18 +1813,13 @@ def do_load(self, file_path):
17751813
return
17761814

17771815
try:
1778-
# Specify file encoding in Python 3, but Python 2 doesn't allow that argument to open()
1779-
if six.PY3:
1780-
# Add all commands in the script to the command queue
1781-
with open(expanded_path, encoding='utf-8') as target:
1782-
self.cmdqueue.extend(target.read().splitlines())
1783-
else:
1784-
# Add all commands in the script to the command queue
1785-
with open(expanded_path) as target:
1786-
self.cmdqueue.extend(target.read().splitlines())
1787-
1788-
# Append in an "end of script (eos)" command to cleanup the self._script_dir list
1789-
self.cmdqueue.append('eos')
1816+
# Read all lines of the script and insert into the head of the
1817+
# command queue. Add an "end of script (eos)" command to cleanup the
1818+
# self._script_dir list when done. Specify file encoding in Python
1819+
# 3, but Python 2 doesn't allow that argument to open().
1820+
kwargs = {'encoding' : 'utf-8'} if six.PY3 else {}
1821+
with open(expanded_path, **kwargs) as target:
1822+
self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
17901823
except IOError as e:
17911824
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
17921825
return

tests/scripts/nested.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
_relative_load precmds.txt
2+
help
3+
shortcuts
4+
_relative_load postcmds.txt

tests/scripts/postcmds.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
set abbrev off
2+
set colors off

tests/scripts/precmds.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
set abbrev on
2+
set colors on

tests/test_cmd2.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,61 @@ def test_load_with_utf8_file(base_app, capsys, request):
401401
assert base_app._current_script_dir == sdir
402402

403403

404+
def test_load_nested_loads(base_app, request):
405+
# Verify that loading a script with nested load commands works correctly,
406+
# and loads the nested script commands in the correct order. The recursive
407+
# loads don't happen all at once, but as the commands are interpreted. So,
408+
# we will need to drain the cmdqueue and inspect the stdout to see if all
409+
# steps were executed in the expected order.
410+
test_dir = os.path.dirname(request.module.__file__)
411+
filename = os.path.join(test_dir, 'scripts', 'nested.txt')
412+
assert base_app.cmdqueue == []
413+
414+
# Load the top level script and then run the command queue until all
415+
# commands have been exhausted.
416+
initial_load = 'load ' + filename
417+
run_cmd(base_app, initial_load)
418+
while base_app.cmdqueue:
419+
base_app.onecmd_plus_hooks(base_app.cmdqueue.pop(0))
420+
421+
# Check that the right commands were executed.
422+
expected = """
423+
%s
424+
_relative_load precmds.txt
425+
set abbrev on
426+
set colors on
427+
help
428+
shortcuts
429+
_relative_load postcmds.txt
430+
set abbrev off
431+
set colors off""" % initial_load
432+
assert run_cmd(base_app, 'history -s') == normalize(expected)
433+
434+
435+
def test_base_runcmds_plus_hooks(base_app, request):
436+
# Make sure that runcmds_plus_hooks works as intended. I.E. to run multiple
437+
# commands and process any commands added, by them, to the command queue.
438+
test_dir = os.path.dirname(request.module.__file__)
439+
prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt')
440+
postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt')
441+
assert base_app.cmdqueue == []
442+
443+
base_app.runcmds_plus_hooks(['load ' + prefilepath,
444+
'help',
445+
'shortcuts',
446+
'load ' + postfilepath])
447+
expected = """
448+
load %s
449+
set abbrev on
450+
set colors on
451+
help
452+
shortcuts
453+
load %s
454+
set abbrev off
455+
set colors off""" % (prefilepath, postfilepath)
456+
assert run_cmd(base_app, 'history -s') == normalize(expected)
457+
458+
404459
def test_base_relative_load(base_app, request):
405460
test_dir = os.path.dirname(request.module.__file__)
406461
filename = os.path.join(test_dir, 'script.txt')

0 commit comments

Comments
 (0)