Skip to content

Commit 309c3ec

Browse files
authored
Merge pull request #153 from kmvanbrunt/text_file
Verifying a file to be loaded as a text script is either ASCII or UTF-8
2 parents 679401b + aa18449 commit 309c3ec

5 files changed

Lines changed: 110 additions & 11 deletions

File tree

cmd2.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Git repository on GitHub at https://github.com/python-cmd2/cmd2
2727
"""
2828
import cmd
29+
import codecs
2930
import collections
3031
import datetime
3132
import glob
@@ -1643,31 +1644,31 @@ def do_edit(self, arg):
16431644
filename = None
16441645
if arg:
16451646
try:
1646-
buffer = self._last_matching(int(arg))
1647+
history_item = self._last_matching(int(arg))
16471648
except ValueError:
16481649
filename = arg
1649-
buffer = ''
1650+
history_item = ''
16501651
else:
16511652
try:
1652-
buffer = self.history[-1]
1653+
history_item = self.history[-1]
16531654
except IndexError:
16541655
self.perror('edit must be called with argument if history is empty', traceback_war=False)
16551656
return
16561657

16571658
delete_tempfile = False
1658-
if buffer:
1659+
if history_item:
16591660
if filename is None:
16601661
fd, filename = tempfile.mkstemp(suffix='.txt', text=True)
16611662
os.close(fd)
16621663
delete_tempfile = True
16631664

16641665
f = open(os.path.expanduser(filename), 'w')
1665-
f.write(buffer or '')
1666+
f.write(history_item or '')
16661667
f.close()
16671668

16681669
os.system('%s %s' % (self.editor, filename))
16691670

1670-
if self.autorun_on_edit or buffer:
1671+
if self.autorun_on_edit or history_item:
16711672
self.do_load(filename)
16721673

16731674
if delete_tempfile:
@@ -1758,6 +1759,22 @@ def do_load(self, file_path):
17581759
return
17591760

17601761
expanded_path = os.path.abspath(os.path.expanduser(file_path.strip()))
1762+
1763+
# Make sure expanded_path points to a file
1764+
if not os.path.isfile(expanded_path):
1765+
self.perror('{} does not exist or is not a file\n'.format(expanded_path), traceback_war=False)
1766+
return
1767+
1768+
# Make sure the file is not empty
1769+
if os.path.getsize(expanded_path) == 0:
1770+
self.perror('{} is empty\n'.format(expanded_path), traceback_war=False)
1771+
return
1772+
1773+
# Make sure the file is ASCII or UTF-8 encoded text
1774+
if not self.is_text_file(expanded_path):
1775+
self.perror('{} is not an ASCII or UTF-8 encoded text file\n'.format(expanded_path), traceback_war=False)
1776+
return
1777+
17611778
try:
17621779
target = open(expanded_path)
17631780
except IOError as e:
@@ -1788,6 +1805,40 @@ def do_run(self, arg):
17881805
if runme:
17891806
return self.onecmd_plus_hooks(runme)
17901807

1808+
@staticmethod
1809+
def is_text_file(file_path):
1810+
"""
1811+
Returns if a file contains only ASCII or UTF-8 encoded text
1812+
:param file_path: path to the file being checked
1813+
"""
1814+
expanded_path = os.path.abspath(os.path.expanduser(file_path.strip()))
1815+
valid_text_file = False
1816+
1817+
# Check if the file is ASCII
1818+
try:
1819+
with codecs.open(expanded_path, encoding='ascii', errors='strict') as f:
1820+
# Make sure the file has at least one line of text
1821+
# noinspection PyUnusedLocal
1822+
if sum(1 for line in f) > 0:
1823+
valid_text_file = True
1824+
except IOError:
1825+
pass
1826+
except UnicodeDecodeError:
1827+
# The file is not ASCII. Check if it is UTF-8.
1828+
try:
1829+
with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f:
1830+
# Make sure the file has at least one line of text
1831+
# noinspection PyUnusedLocal
1832+
if sum(1 for line in f) > 0:
1833+
valid_text_file = True
1834+
except IOError:
1835+
pass
1836+
except UnicodeDecodeError:
1837+
# Not UTF-8
1838+
pass
1839+
1840+
return valid_text_file
1841+
17911842
def run_transcript_tests(self, callargs):
17921843
"""Runs transcript tests for provided file(s).
17931844
@@ -2357,5 +2408,4 @@ def __bool__(self):
23572408
if __name__ == '__main__':
23582409
# If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality.
23592410
app = Cmd()
2360-
app.debug = True
23612411
app.cmdloop()

tests/scripts/binary.bin

51 Bytes
Binary file not shown.

tests/scripts/empty.txt

Whitespace-only changes.

tests/scripts/utf8.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!echo γνωρίζω

tests/test_cmd2.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,54 @@ def test_base_load_with_empty_args(base_app, capsys):
293293
assert normalize(str(err)) == expected
294294

295295

296+
def test_base_load_with_nonexistent_file(base_app, capsys):
297+
# The way the load command works, we can't directly capture its stdout or stderr
298+
run_cmd(base_app, 'load does_not_exist.txt')
299+
out, err = capsys.readouterr()
300+
301+
# The load command requires a path to an existing file
302+
assert str(err).startswith("ERROR")
303+
assert "does not exist or is not a file" in str(err)
304+
305+
306+
def test_base_load_with_empty_file(base_app, capsys, request):
307+
test_dir = os.path.dirname(request.module.__file__)
308+
filename = os.path.join(test_dir, 'scripts', 'empty.txt')
309+
310+
# The way the load command works, we can't directly capture its stdout or stderr
311+
run_cmd(base_app, 'load {}'.format(filename))
312+
out, err = capsys.readouterr()
313+
314+
# The load command requires non-empty scripts files
315+
assert str(err).startswith("ERROR")
316+
assert "is empty" in str(err)
317+
318+
319+
def test_base_load_with_binary_file(base_app, capsys, request):
320+
test_dir = os.path.dirname(request.module.__file__)
321+
filename = os.path.join(test_dir, 'scripts', 'binary.bin')
322+
323+
# The way the load command works, we can't directly capture its stdout or stderr
324+
run_cmd(base_app, 'load {}'.format(filename))
325+
out, err = capsys.readouterr()
326+
327+
# The load command requires non-empty scripts files
328+
assert str(err).startswith("ERROR")
329+
assert "is not an ASCII or UTF-8 encoded text file" in str(err)
330+
331+
332+
def test_base_load_with_utf8_file(base_app, capsys, request):
333+
test_dir = os.path.dirname(request.module.__file__)
334+
filename = os.path.join(test_dir, 'scripts', 'utf8.txt')
335+
336+
# The way the load command works, we can't directly capture its stdout or stderr
337+
run_cmd(base_app, 'load {}'.format(filename))
338+
out, err = capsys.readouterr()
339+
340+
# TODO Make this test better once shell command is fixed to used cmd2's stdout
341+
assert str(err) == ''
342+
343+
296344
def test_base_relative_load(base_app, request):
297345
test_dir = os.path.dirname(request.module.__file__)
298346
filename = os.path.join(test_dir, 'script.txt')
@@ -458,10 +506,10 @@ def test_base_timing(base_app, capsys):
458506

459507

460508
def test_base_debug(base_app, capsys):
461-
# Try to load a non-existent file with debug set to False by default
462-
run_cmd(base_app, 'load does_not_exist.txt')
509+
# Try to set a non-existent parameter with debug set to False by default
510+
run_cmd(base_app, 'set does_not_exist 5')
463511
out, err = capsys.readouterr()
464-
assert err.startswith('ERROR')
512+
assert err.startswith('EXCEPTION')
465513

466514
# Set debug true
467515
out = run_cmd(base_app, 'set debug True')
@@ -472,7 +520,7 @@ def test_base_debug(base_app, capsys):
472520
assert out == expected
473521

474522
# Verify that we now see the exception traceback
475-
run_cmd(base_app, 'load does_not_exist.txt')
523+
run_cmd(base_app, 'set does_not_exist 5')
476524
out, err = capsys.readouterr()
477525
assert str(err).startswith('Traceback (most recent call last):')
478526

0 commit comments

Comments
 (0)