summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlfred Levy <alevy03@gmail.com>2017-09-22 13:58:17 -0400
committerAlfred Levy <alevy03@gmail.com>2017-09-22 13:58:17 -0400
commit6fa589b3c50352eea2d15f3ad69c91c5118f71d8 (patch)
tree023942c4ca32c7385717cff66f2621052893b324
parent6d711bcb104b5afd0bfb49cbe30410ec5b8fadd5 (diff)
downloadcmd2-git-6fa589b3c50352eea2d15f3ad69c91c5118f71d8.tar.gz
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.
-rw-r--r--CONTRIBUTING.md2
-rwxr-xr-xcmd2.py57
-rw-r--r--tests/scripts/nested.txt4
-rw-r--r--tests/scripts/postcmds.txt2
-rw-r--r--tests/scripts/precmds.txt2
-rw-r--r--tests/test_cmd2.py55
6 files changed, 109 insertions, 13 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 88898f8d..5eace248 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
# Install prerequisites for running cmd2 unit tests
pip install -U pytest mock
diff --git a/cmd2.py b/cmd2.py
index 87fb3aad..b0554eb5 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -789,6 +789,44 @@ class Cmd(cmd.Cmd):
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)):
@@ -1775,18 +1813,13 @@ Script should contain one command per line, just like command would be typed in
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
diff --git a/tests/scripts/nested.txt b/tests/scripts/nested.txt
new file mode 100644
index 00000000..ac3b4748
--- /dev/null
+++ b/tests/scripts/nested.txt
@@ -0,0 +1,4 @@
+_relative_load precmds.txt
+help
+shortcuts
+_relative_load postcmds.txt
diff --git a/tests/scripts/postcmds.txt b/tests/scripts/postcmds.txt
new file mode 100644
index 00000000..760bcdf5
--- /dev/null
+++ b/tests/scripts/postcmds.txt
@@ -0,0 +1,2 @@
+set abbrev off
+set colors off
diff --git a/tests/scripts/precmds.txt b/tests/scripts/precmds.txt
new file mode 100644
index 00000000..d8857e92
--- /dev/null
+++ b/tests/scripts/precmds.txt
@@ -0,0 +1,2 @@
+set abbrev on
+set colors on
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 4f65606d..68924e08 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -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')