summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--cmd2/cmd2.py43
-rw-r--r--docs/freefeatures.rst9
-rw-r--r--docs/transcript.rst17
-rw-r--r--tests/scripts/help.txt1
-rw-r--r--tests/test_transcript.py35
6 files changed, 88 insertions, 18 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c35a182b..da3d0751 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
* The `with_argparser` decorators now add the Statement object created when parsing the command line to the
`argparse.Namespace` object they pass to the `do_*` methods. It is stored in an attribute called `__statement__`.
This can be useful if a command function needs to know the command line for things like logging.
+ * Added a `-t` option to the `load` command for automatically generating a transcript based on a script file
* Potentially breaking changes
* The following commands now write to stderr instead of stdout when printing an error. This will make catching
errors easier in pyscript.
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 33cfe3fc..0aaeb4c3 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -3350,18 +3350,22 @@ class Cmd(cmd.Cmd):
for hi in history:
self.poutput(hi.pr(script=args.script, expanded=args.expanded, verbose=args.verbose))
- def _generate_transcript(self, history: List[HistoryItem], transcript_file: str) -> None:
+ def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None:
"""Generate a transcript file from a given history of commands."""
- # Save the current echo state, and turn it off. We inject commands into the
- # output using a different mechanism
import io
+ # Validate the transcript file path to make sure directory exists and write access is available
+ transcript_path = os.path.abspath(os.path.expanduser(transcript_file))
+ transcript_dir = os.path.dirname(transcript_path)
+ if not os.path.isdir(transcript_dir) or not os.access(transcript_dir, os.W_OK):
+ self.perror("{!r} is not a directory or you don't have write access".format(transcript_dir),
+ traceback_war=False)
+ return
+ # Disable echo while we manually redirect stdout to a StringIO buffer
saved_echo = self.echo
+ saved_stdout = self.stdout
self.echo = False
- # Redirect stdout to the transcript file
- saved_self_stdout = self.stdout
-
# The problem with supporting regular expressions in transcripts
# is that they shouldn't be processed in the command, just the output.
# In addition, when we generate a transcript, any slashes in the output
@@ -3397,10 +3401,9 @@ class Cmd(cmd.Cmd):
# and add the regex-escaped output to the transcript
transcript += output.replace('/', r'\/')
- # Restore stdout to its original state
- self.stdout = saved_self_stdout
- # Set echo back to its original state
+ # Restore altered attributes to their original state
self.echo = saved_echo
+ self.stdout = saved_stdout
# finally, we can write the transcript out to the file
try:
@@ -3456,9 +3459,20 @@ class Cmd(cmd.Cmd):
load_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
"\n"
"Script should contain one command per line, just like the command would be\n"
- "typed in the console.")
+ "typed in the console.\n"
+ "\n"
+ "It loads commands from a script file into a queue and then the normal cmd2\n"
+ "REPL resumes control and executes the commands in the queue in FIFO order.\n"
+ "If you attempt to redirect/pipe a load command, it will capture the output\n"
+ "of the load command itself, not what it adds to the queue.\n"
+ "\n"
+ "If the -r/--record_transcript flag is used, this command instead records\n"
+ "the output of the script commands to a transcript for testing purposes.\n"
+ )
load_parser = ACArgumentParser(description=load_description)
+ setattr(load_parser.add_argument('-t', '--transcript', help='record the output of the script as a transcript file'),
+ ACTION_ARG_CHOICES, ('path_complete',))
setattr(load_parser.add_argument('script_path', help="path to the script file"),
ACTION_ARG_CHOICES, ('path_complete',))
@@ -3492,11 +3506,16 @@ class Cmd(cmd.Cmd):
# command queue. Add an "end of script (eos)" command to cleanup the
# self._script_dir list when done.
with open(expanded_path, encoding='utf-8') as target:
- self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue
+ script_commands = target.read().splitlines()
except OSError as ex: # pragma: no cover
self.perror("Problem accessing script from '{}': {}".format(expanded_path, ex))
return
+ if args.transcript:
+ self._generate_transcript(script_commands, os.path.expanduser(args.transcript))
+ return
+
+ self.cmdqueue = script_commands + ['eos'] + self.cmdqueue
self._script_dir.append(os.path.dirname(expanded_path))
relative_load_description = load_description
@@ -3812,7 +3831,7 @@ class Cmd(cmd.Cmd):
# If transcript-based regression testing was requested, then do that instead of the main loop
if self._transcript_files is not None:
- self.run_transcript_tests(self._transcript_files)
+ self.run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files])
else:
# If an intro was supplied in the method call, allow it to override the default
if intro is not None:
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index 001a7599..11b5de68 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -19,6 +19,15 @@ Both ASCII and UTF-8 encoded unicode text files are supported.
Simply include one command per line, typed exactly as you would inside a ``cmd2`` application.
+The ``load`` command loads commands from a script file into a queue and then the normal cmd2 REPL
+resumes control and executes the commands in the queue in FIFO order. A side effect of this
+is that if you redirect/pipe the output of a load command, it will redirect the output of the ``load``
+command itself, but will NOT redirect the output of the command loaded from the script file. Of course,
+you can add redirection to the commands being run in the script file, e.g.::
+
+ # This is your script file
+ command arg1 arg2 > file.txt
+
.. automethod:: cmd2.cmd2.Cmd.do_load
.. automethod:: cmd2.cmd2.Cmd.do__relative_load
diff --git a/docs/transcript.rst b/docs/transcript.rst
index 36b35fcf..c7c31fd6 100644
--- a/docs/transcript.rst
+++ b/docs/transcript.rst
@@ -13,9 +13,9 @@ from commands that produce dynamic or variable output.
Creating a transcript
=====================
-Automatically
--------------
-A transcript can automatically generated based upon commands previously executed in the *history*::
+Automatically from history
+--------------------------
+A transcript can automatically generated based upon commands previously executed in the *history* using ``history -t``::
(Cmd) help
...
@@ -32,6 +32,17 @@ This is by far the easiest way to generate a transcript.
of the ``cmd2.Cmd`` class ensure that output is properly redirected when redirecting to a file, piping to a shell
command, and when generating a transcript.
+Automatically from a script file
+--------------------------------
+A transcript can also be automatically generated from a script file using ``load -t``::
+
+ (Cmd) load scripts/script.txt -t transcript.txt
+ 2 commands and their outputs saved to transcript file 'transcript.txt'
+ (Cmd)
+
+This is a particularly attractive option for automatically regenerating transcripts for regression testing as your ``cmd2``
+application changes.
+
Manually
--------
Here's a transcript created from ``python examples/example.py``::
diff --git a/tests/scripts/help.txt b/tests/scripts/help.txt
new file mode 100644
index 00000000..71d23d97
--- /dev/null
+++ b/tests/scripts/help.txt
@@ -0,0 +1 @@
+help -v
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 4a03ebe0..acdbe703 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -17,7 +17,7 @@ from unittest import mock
import pytest
import cmd2
-from .conftest import run_cmd
+from .conftest import run_cmd, BASE_HELP_VERBOSE
from cmd2 import transcript
from cmd2.utils import StdSim
@@ -136,7 +136,7 @@ def test_transcript(request, capsys, filename, feedback_to_output):
assert err.startswith(expected_start)
assert err.endswith(expected_end)
-def test_history_transcript(request, capsys):
+def test_history_transcript():
app = CmdLineApp()
app.stdout = StdSim(app.stdout)
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
@@ -163,7 +163,7 @@ this is a \/multiline\/ command
assert xscript == expected
-def test_history_transcript_bad_filename(request, capsys):
+def test_history_transcript_bad_filename():
app = CmdLineApp()
app.stdout = StdSim(app.stdout)
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
@@ -189,6 +189,35 @@ this is a \/multiline\/ command
transcript = f.read()
assert transcript == expected
+
+def test_load_record_transcript(base_app, request):
+ test_dir = os.path.dirname(request.module.__file__)
+ filename = os.path.join(test_dir, 'scripts', 'help.txt')
+
+ assert base_app.cmdqueue == []
+ assert base_app._script_dir == []
+ assert base_app._current_script_dir is None
+
+ # make a tmp file to use as a transcript
+ fd, transcript_fname = tempfile.mkstemp(prefix='', suffix='.trn')
+ os.close(fd)
+
+ # Run the load command with the -r option to generate a transcript
+ run_cmd(base_app, 'load {} -t {}'.format(filename, transcript_fname))
+
+ assert base_app.cmdqueue == []
+ assert base_app._script_dir == []
+ assert base_app._current_script_dir is None
+
+ # read in the transcript created by the history command
+ with open(transcript_fname) as f:
+ xscript = f.read()
+
+ expected = '(Cmd) help -v\n' + BASE_HELP_VERBOSE + '\n'
+
+ assert xscript == expected
+
+
@pytest.mark.parametrize('expected, transformed', [
# strings with zero or one slash or with escaped slashes means no regular
# expression present, so the result should just be what re.escape returns.