diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rwxr-xr-x | cmd2.py | 106 | ||||
| -rw-r--r-- | docs/conf.py | 2 | ||||
| -rwxr-xr-x | setup.py | 2 | ||||
| -rw-r--r-- | tests/test_cmd2.py | 40 | ||||
| -rw-r--r-- | tests/test_transcript.py | 27 |
6 files changed, 147 insertions, 34 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index a449d97c..fc7f150c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.8 (TBD, 2018) +* Bug Fixes + * Prevent crashes that could occur attempting to open a file in non-existent directory or with very long filename + ## 0.8.7 (May 28, 2018) * Bug Fixes * Make sure pip installs version 0.8.x if you have python 2.7 @@ -229,7 +229,7 @@ if six.PY2 and sys.platform.startswith('lin'): pass -__version__ = '0.8.7' +__version__ = '0.8.8' # Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past pyparsing.ParserElement.enablePackrat() @@ -2470,7 +2470,7 @@ class Cmd(cmd.Cmd): if self.timing: self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart)) finally: - if self.allow_redirection: + if self.allow_redirection and self.redirecting: self._restore_output(statement) except EmptyStatement: pass @@ -2586,7 +2586,11 @@ class Cmd(cmd.Cmd): mode = 'w' if statement.parsed.output == 2 * self.redirector: mode = 'a' - sys.stdout = self.stdout = open(os.path.expanduser(statement.parsed.outputTo), mode) + try: + sys.stdout = self.stdout = open(os.path.expanduser(statement.parsed.outputTo), mode) + except (FILE_NOT_FOUND_ERROR, IOError) as ex: + self.perror('Not Redirecting because - {}'.format(ex), traceback_war=False) + self.redirecting = False else: sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+") if statement.parsed.output == '>>': @@ -3638,34 +3642,7 @@ a..b, a:b, a:, ..b items by indices (inclusive) except Exception as e: self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False) elif args.transcript: - # Make sure echo is on so commands print to standard out - saved_echo = self.echo - self.echo = True - - # Redirect stdout to the transcript file - saved_self_stdout = self.stdout - self.stdout = open(args.transcript, 'w') - - # Run all of the commands in the history with output redirected to transcript and echo on - self.runcmds_plus_hooks(history) - - # Restore stdout to its original state - self.stdout.close() - self.stdout = saved_self_stdout - - # Set echo back to its original state - self.echo = saved_echo - - # Post-process the file to escape un-escaped "/" regex escapes - with open(args.transcript, 'r') as fin: - data = fin.read() - post_processed_data = data.replace('/', '\/') - with open(args.transcript, 'w') as fout: - fout.write(post_processed_data) - - plural = 's' if len(history) > 1 else '' - self.pfeedback('{} command{} and outputs saved to transcript file {!r}'.format(len(history), plural, - args.transcript)) + self._generate_transcript(history, args.transcript) else: # Display the history items retrieved for hi in history: @@ -3674,6 +3651,73 @@ a..b, a:b, a:, ..b items by indices (inclusive) else: self.poutput(hi.pr()) + def _generate_transcript(self, history, transcript_file): + """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 + + saved_echo = self.echo + 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 + # are not really intended to indicate regular expressions, so they should + # be escaped. + # + # We have to jump through some hoops here in order to catch the commands + # separately from the output and escape the slashes in the output. + transcript = '' + for history_item in history: + # build the command, complete with prompts. When we replay + # the transcript, we look for the prompts to separate + # the command from the output + first = True + command = '' + for line in history_item.splitlines(): + if first: + command += '{}{}\n'.format(self.prompt, line) + first = False + else: + command += '{}{}\n'.format(self.continuation_prompt, line) + transcript += command + # create a new string buffer and set it to stdout to catch the output + # of the command + membuf = io.StringIO() + self.stdout = membuf + # then run the command and let the output go into our buffer + self.onecmd_plus_hooks(history_item) + # rewind the buffer to the beginning + membuf.seek(0) + # get the output out of the buffer + output = membuf.read() + # and add the regex-escaped output to the transcript + transcript += output.replace('/', '\/') + + # Restore stdout to its original state + self.stdout = saved_self_stdout + # Set echo back to its original state + self.echo = saved_echo + + # finally, we can write the transcript out to the file + try: + with open(transcript_file, 'w') as fout: + fout.write(transcript) + except (FILE_NOT_FOUND_ERROR, IOError) as ex: + self.perror('Failed to save transcript: {}'.format(ex), traceback_war=False) + else: + # and let the user know what we did + if len(history) > 1: + plural = 'commands and their outputs' + else: + plural = 'command and its output' + msg = '{} {} saved to transcript file {!r}' + self.pfeedback(msg.format(len(history), plural, transcript_file)) + @with_argument_list def do_edit(self, arglist): """Edit a file in a text editor. diff --git a/docs/conf.py b/docs/conf.py index e81205b3..71dff86e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,7 +62,7 @@ author = 'Catherine Devlin and Todd Leonhardt' # The short X.Y version. version = '0.8' # The full version, including alpha/beta/rc tags. -release = '0.8.7' +release = '0.8.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -8,7 +8,7 @@ import sys import setuptools from setuptools import setup -VERSION = '0.8.7' +VERSION = '0.8.8' DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python" LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 66e4d601..17577e2b 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -26,7 +26,7 @@ from conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \ def test_ver(): - assert cmd2.__version__ == '0.8.7' + assert cmd2.__version__ == '0.8.8' def test_empty_statement(base_app): @@ -553,6 +553,44 @@ def test_output_redirection(base_app): finally: os.remove(filename) +def test_output_redirection_to_nonexistent_directory(base_app): + filename = '~/fakedir/this_does_not_exist.txt' + + # Verify that writing to a file in a non-existent directory doesn't work + run_cmd(base_app, 'help > {}'.format(filename)) + expected = normalize(BASE_HELP) + with pytest.raises(cmd2.FILE_NOT_FOUND_ERROR): + with open(filename) as f: + content = normalize(f.read()) + assert content == expected + + # Verify that appending to a file also works + run_cmd(base_app, 'help history >> {}'.format(filename)) + expected = normalize(BASE_HELP + '\n' + HELP_HISTORY) + with pytest.raises(cmd2.FILE_NOT_FOUND_ERROR): + with open(filename) as f: + content = normalize(f.read()) + assert content == expected + +def test_output_redirection_to_too_long_filename(base_app): + filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfiuewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiuewhfiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheiufheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehiuewhfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' + + # Verify that writing to a file in a non-existent directory doesn't work + run_cmd(base_app, 'help > {}'.format(filename)) + expected = normalize(BASE_HELP) + with pytest.raises(IOError): + with open(filename) as f: + content = normalize(f.read()) + assert content == expected + + # Verify that appending to a file also works + run_cmd(base_app, 'help history >> {}'.format(filename)) + expected = normalize(BASE_HELP + '\n' + HELP_HISTORY) + with pytest.raises(IOError): + with open(filename) as f: + content = normalize(f.read()) + assert content == expected + def test_feedback_to_output_true(base_app): base_app.feedback_to_output = True diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 8c2af29d..d1cf768e 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -14,6 +14,7 @@ import mock import pytest import six +import cmd2 from cmd2 import (Cmd, options, Cmd2TestCase, set_use_arg_list, set_posix_shlex, set_strip_quotes) from conftest import run_cmd, StdOut, normalize @@ -305,6 +306,32 @@ def test_transcript(request, capsys, filename, feedback_to_output): assert out == '' +def test_history_transcript_bad_filename(request, capsys): + app = CmdLineApp() + app.stdout = StdOut() + run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') + run_cmd(app, 'speak /tmp/file.txt is not a regex') + + expected = r"""(Cmd) orate this is +> a /multiline/ +> command; +this is a \/multiline\/ command +(Cmd) speak /tmp/file.txt is not a regex +\/tmp\/file.txt is not a regex +""" + + # make a tmp file + history_fname = '~/fakedir/this_does_not_exist.txt' + + # tell the history command to create a transcript + run_cmd(app, 'history -t "{}"'.format(history_fname)) + + # read in the transcript created by the history command + with pytest.raises(cmd2.FILE_NOT_FOUND_ERROR): + with open(history_fname) as f: + transcript = f.read() + assert transcript == 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. |
