Skip to content

Commit 91a69cb

Browse files
committed
Backported file open crash fixes for Issue #430 to python2 branch
Also: - Bumped version on python2 branch to 0.8.8 - Updated CHANGELOG
1 parent 05e1598 commit 91a69cb

File tree

6 files changed

+147
-34
lines changed

6 files changed

+147
-34
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.8 (TBD, 2018)
2+
* Bug Fixes
3+
* Prevent crashes that could occur attempting to open a file in non-existent directory or with very long filename
4+
15
## 0.8.7 (May 28, 2018)
26
* Bug Fixes
37
* Make sure pip installs version 0.8.x if you have python 2.7

cmd2.py

+75-31
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def pyreadline_remove_history_item(pos):
229229
pass
230230

231231

232-
__version__ = '0.8.7'
232+
__version__ = '0.8.8'
233233

234234
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
235235
pyparsing.ParserElement.enablePackrat()
@@ -2470,7 +2470,7 @@ def onecmd_plus_hooks(self, line):
24702470
if self.timing:
24712471
self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart))
24722472
finally:
2473-
if self.allow_redirection:
2473+
if self.allow_redirection and self.redirecting:
24742474
self._restore_output(statement)
24752475
except EmptyStatement:
24762476
pass
@@ -2586,7 +2586,11 @@ def _redirect_output(self, statement):
25862586
mode = 'w'
25872587
if statement.parsed.output == 2 * self.redirector:
25882588
mode = 'a'
2589-
sys.stdout = self.stdout = open(os.path.expanduser(statement.parsed.outputTo), mode)
2589+
try:
2590+
sys.stdout = self.stdout = open(os.path.expanduser(statement.parsed.outputTo), mode)
2591+
except (FILE_NOT_FOUND_ERROR, IOError) as ex:
2592+
self.perror('Not Redirecting because - {}'.format(ex), traceback_war=False)
2593+
self.redirecting = False
25902594
else:
25912595
sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+")
25922596
if statement.parsed.output == '>>':
@@ -3638,34 +3642,7 @@ def do_history(self, args):
36383642
except Exception as e:
36393643
self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False)
36403644
elif args.transcript:
3641-
# Make sure echo is on so commands print to standard out
3642-
saved_echo = self.echo
3643-
self.echo = True
3644-
3645-
# Redirect stdout to the transcript file
3646-
saved_self_stdout = self.stdout
3647-
self.stdout = open(args.transcript, 'w')
3648-
3649-
# Run all of the commands in the history with output redirected to transcript and echo on
3650-
self.runcmds_plus_hooks(history)
3651-
3652-
# Restore stdout to its original state
3653-
self.stdout.close()
3654-
self.stdout = saved_self_stdout
3655-
3656-
# Set echo back to its original state
3657-
self.echo = saved_echo
3658-
3659-
# Post-process the file to escape un-escaped "/" regex escapes
3660-
with open(args.transcript, 'r') as fin:
3661-
data = fin.read()
3662-
post_processed_data = data.replace('/', '\/')
3663-
with open(args.transcript, 'w') as fout:
3664-
fout.write(post_processed_data)
3665-
3666-
plural = 's' if len(history) > 1 else ''
3667-
self.pfeedback('{} command{} and outputs saved to transcript file {!r}'.format(len(history), plural,
3668-
args.transcript))
3645+
self._generate_transcript(history, args.transcript)
36693646
else:
36703647
# Display the history items retrieved
36713648
for hi in history:
@@ -3674,6 +3651,73 @@ def do_history(self, args):
36743651
else:
36753652
self.poutput(hi.pr())
36763653

3654+
def _generate_transcript(self, history, transcript_file):
3655+
"""Generate a transcript file from a given history of commands."""
3656+
# Save the current echo state, and turn it off. We inject commands into the
3657+
# output using a different mechanism
3658+
import io
3659+
3660+
saved_echo = self.echo
3661+
self.echo = False
3662+
3663+
# Redirect stdout to the transcript file
3664+
saved_self_stdout = self.stdout
3665+
3666+
# The problem with supporting regular expressions in transcripts
3667+
# is that they shouldn't be processed in the command, just the output.
3668+
# In addition, when we generate a transcript, any slashes in the output
3669+
# are not really intended to indicate regular expressions, so they should
3670+
# be escaped.
3671+
#
3672+
# We have to jump through some hoops here in order to catch the commands
3673+
# separately from the output and escape the slashes in the output.
3674+
transcript = ''
3675+
for history_item in history:
3676+
# build the command, complete with prompts. When we replay
3677+
# the transcript, we look for the prompts to separate
3678+
# the command from the output
3679+
first = True
3680+
command = ''
3681+
for line in history_item.splitlines():
3682+
if first:
3683+
command += '{}{}\n'.format(self.prompt, line)
3684+
first = False
3685+
else:
3686+
command += '{}{}\n'.format(self.continuation_prompt, line)
3687+
transcript += command
3688+
# create a new string buffer and set it to stdout to catch the output
3689+
# of the command
3690+
membuf = io.StringIO()
3691+
self.stdout = membuf
3692+
# then run the command and let the output go into our buffer
3693+
self.onecmd_plus_hooks(history_item)
3694+
# rewind the buffer to the beginning
3695+
membuf.seek(0)
3696+
# get the output out of the buffer
3697+
output = membuf.read()
3698+
# and add the regex-escaped output to the transcript
3699+
transcript += output.replace('/', '\/')
3700+
3701+
# Restore stdout to its original state
3702+
self.stdout = saved_self_stdout
3703+
# Set echo back to its original state
3704+
self.echo = saved_echo
3705+
3706+
# finally, we can write the transcript out to the file
3707+
try:
3708+
with open(transcript_file, 'w') as fout:
3709+
fout.write(transcript)
3710+
except (FILE_NOT_FOUND_ERROR, IOError) as ex:
3711+
self.perror('Failed to save transcript: {}'.format(ex), traceback_war=False)
3712+
else:
3713+
# and let the user know what we did
3714+
if len(history) > 1:
3715+
plural = 'commands and their outputs'
3716+
else:
3717+
plural = 'command and its output'
3718+
msg = '{} {} saved to transcript file {!r}'
3719+
self.pfeedback(msg.format(len(history), plural, transcript_file))
3720+
36773721
@with_argument_list
36783722
def do_edit(self, arglist):
36793723
"""Edit a file in a text editor.

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
# The short X.Y version.
6363
version = '0.8'
6464
# The full version, including alpha/beta/rc tags.
65-
release = '0.8.7'
65+
release = '0.8.8'
6666

6767
# The language for content autogenerated by Sphinx. Refer to documentation
6868
# for a list of supported languages.

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import setuptools
99
from setuptools import setup
1010

11-
VERSION = '0.8.7'
11+
VERSION = '0.8.8'
1212
DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python"
1313
LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make
1414
it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It

tests/test_cmd2.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727

2828
def test_ver():
29-
assert cmd2.__version__ == '0.8.7'
29+
assert cmd2.__version__ == '0.8.8'
3030

3131

3232
def test_empty_statement(base_app):
@@ -553,6 +553,44 @@ def test_output_redirection(base_app):
553553
finally:
554554
os.remove(filename)
555555

556+
def test_output_redirection_to_nonexistent_directory(base_app):
557+
filename = '~/fakedir/this_does_not_exist.txt'
558+
559+
# Verify that writing to a file in a non-existent directory doesn't work
560+
run_cmd(base_app, 'help > {}'.format(filename))
561+
expected = normalize(BASE_HELP)
562+
with pytest.raises(cmd2.FILE_NOT_FOUND_ERROR):
563+
with open(filename) as f:
564+
content = normalize(f.read())
565+
assert content == expected
566+
567+
# Verify that appending to a file also works
568+
run_cmd(base_app, 'help history >> {}'.format(filename))
569+
expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
570+
with pytest.raises(cmd2.FILE_NOT_FOUND_ERROR):
571+
with open(filename) as f:
572+
content = normalize(f.read())
573+
assert content == expected
574+
575+
def test_output_redirection_to_too_long_filename(base_app):
576+
filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfiuewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiuewhfiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheiufheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehiuewhfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw'
577+
578+
# Verify that writing to a file in a non-existent directory doesn't work
579+
run_cmd(base_app, 'help > {}'.format(filename))
580+
expected = normalize(BASE_HELP)
581+
with pytest.raises(IOError):
582+
with open(filename) as f:
583+
content = normalize(f.read())
584+
assert content == expected
585+
586+
# Verify that appending to a file also works
587+
run_cmd(base_app, 'help history >> {}'.format(filename))
588+
expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
589+
with pytest.raises(IOError):
590+
with open(filename) as f:
591+
content = normalize(f.read())
592+
assert content == expected
593+
556594

557595
def test_feedback_to_output_true(base_app):
558596
base_app.feedback_to_output = True

tests/test_transcript.py

+27
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import pytest
1515
import six
1616

17+
import cmd2
1718
from cmd2 import (Cmd, options, Cmd2TestCase, set_use_arg_list,
1819
set_posix_shlex, set_strip_quotes)
1920
from conftest import run_cmd, StdOut, normalize
@@ -305,6 +306,32 @@ def test_transcript(request, capsys, filename, feedback_to_output):
305306
assert out == ''
306307

307308

309+
def test_history_transcript_bad_filename(request, capsys):
310+
app = CmdLineApp()
311+
app.stdout = StdOut()
312+
run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
313+
run_cmd(app, 'speak /tmp/file.txt is not a regex')
314+
315+
expected = r"""(Cmd) orate this is
316+
> a /multiline/
317+
> command;
318+
this is a \/multiline\/ command
319+
(Cmd) speak /tmp/file.txt is not a regex
320+
\/tmp\/file.txt is not a regex
321+
"""
322+
323+
# make a tmp file
324+
history_fname = '~/fakedir/this_does_not_exist.txt'
325+
326+
# tell the history command to create a transcript
327+
run_cmd(app, 'history -t "{}"'.format(history_fname))
328+
329+
# read in the transcript created by the history command
330+
with pytest.raises(cmd2.FILE_NOT_FOUND_ERROR):
331+
with open(history_fname) as f:
332+
transcript = f.read()
333+
assert transcript == expected
334+
308335
@pytest.mark.parametrize('expected, transformed', [
309336
# strings with zero or one slash or with escaped slashes means no regular
310337
# expression present, so the result should just be what re.escape returns.

0 commit comments

Comments
 (0)