summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rwxr-xr-xcmd2.py106
-rw-r--r--docs/conf.py2
-rwxr-xr-xsetup.py2
-rw-r--r--tests/test_cmd2.py40
-rw-r--r--tests/test_transcript.py27
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
diff --git a/cmd2.py b/cmd2.py
index c5bde284..46be8e95 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -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.
diff --git a/setup.py b/setup.py
index 679e6137..178bbb7b 100755
--- a/setup.py
+++ b/setup.py
@@ -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.