Skip to content

Commit 22c43d7

Browse files
authored
Merge pull request #177 from python-cmd2/cmd_queue
Added an eos (end of script) hidden command and load now populates the cmdqueue
2 parents f3968b6 + 400b831 commit 22c43d7

File tree

3 files changed

+98
-68
lines changed

3 files changed

+98
-68
lines changed

cmd2.py

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,8 @@ class Cmd(cmd.Cmd):
416416
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
417417
allow_redirection = True # Should output redirection and pipes be allowed
418418
default_to_shell = False # Attempt to run unrecognized commands as shell commands
419-
excludeFromHistory = '''run ru r history histor histo hist his hi h edit edi ed e eof eo'''.split()
420-
exclude_from_help = ['do_eof'] # Commands to exclude from the help menu
419+
excludeFromHistory = '''run ru r history histor histo hist his hi h edit edi ed e eof eo eos'''.split()
420+
exclude_from_help = ['do_eof', 'do_eos'] # Commands to exclude from the help menu
421421
reserved_words = []
422422

423423
# Attributes which ARE dynamically settable at runtime
@@ -507,8 +507,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
507507
self._temp_filename = None
508508

509509
# Codes used for exit conditions
510-
self._STOP_AND_EXIT = True # distinguish end of script file from actual exit
511-
self._STOP_SCRIPT_NO_EXIT = -999
510+
self._STOP_AND_EXIT = True # cmd convention
512511

513512
self._colorcodes = {'bold': {True: '\x1b[1m', False: '\x1b[22m'},
514513
'cyan': {True: '\x1b[36m', False: '\x1b[39m'},
@@ -519,8 +518,8 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
519518
'underline': {True: '\x1b[4m', False: '\x1b[24m'},
520519
'yellow': {True: '\x1b[33m', False: '\x1b[39m'}}
521520

522-
# Used by load and _relative_load commands
523-
self._current_script_dir = None
521+
# Used load command to store the current script dir as a LIFO queue to support _relative_load command
522+
self._script_dir = []
524523

525524
# ----- Methods related to presenting output to the user -----
526525

@@ -829,7 +828,6 @@ def onecmd(self, line):
829828
:return: bool - a flag indicating whether the interpretation of commands should stop
830829
"""
831830
statement = self.parser_manager.parsed(line)
832-
self.lastcmd = statement.parsed.raw
833831
funcname = self._func_named(statement.parsed.command)
834832
if not funcname:
835833
return self._default(statement)
@@ -1020,9 +1018,9 @@ def do_shortcuts(self, args):
10201018

10211019
# noinspection PyUnusedLocal
10221020
def do_eof(self, arg):
1023-
"""Automatically called at end of loading a script or when <Ctrl>-D is pressed."""
1021+
"""Called when <Ctrl>-D is pressed."""
10241022
# End of script should not exit app, but <Ctrl>-D should.
1025-
return self._STOP_SCRIPT_NO_EXIT
1023+
return self._STOP_AND_EXIT
10261024

10271025
def do_quit(self, arg):
10281026
"""Exits this application."""
@@ -1591,6 +1589,14 @@ def do_save(self, arg):
15911589
except Exception as e:
15921590
self.perror('Saving {!r} - {}'.format(fname, e), traceback_war=False)
15931591

1592+
@property
1593+
def _current_script_dir(self):
1594+
"""Accessor to get the current script directory from the _script_dir LIFO queue."""
1595+
if self._script_dir:
1596+
return self._script_dir[-1]
1597+
else:
1598+
return None
1599+
15941600
def do__relative_load(self, file_path):
15951601
"""Runs commands in script file that is encoded as either ASCII or UTF-8 text.
15961602
@@ -1616,6 +1622,11 @@ def do__relative_load(self, file_path):
16161622
relative_path = os.path.join(self._current_script_dir or '', file_path)
16171623
self.do_load(relative_path)
16181624

1625+
def do_eos(self, _):
1626+
"""Handles cleanup when a script has finished executing."""
1627+
if self._script_dir:
1628+
self._script_dir.pop()
1629+
16191630
def do_load(self, file_path):
16201631
"""Runs commands in script file that is encoded as either ASCII or UTF-8 text.
16211632
@@ -1648,22 +1659,23 @@ def do_load(self, file_path):
16481659
return
16491660

16501661
try:
1651-
target = open(expanded_path)
1662+
# Specify file encoding in Python 3, but Python 2 doesn't allow that argument to open()
1663+
if six.PY3:
1664+
# Add all commands in the script to the command queue
1665+
with open(expanded_path, encoding='utf-8') as target:
1666+
self.cmdqueue.extend(target.read().splitlines())
1667+
else:
1668+
# Add all commands in the script to the command queue
1669+
with open(expanded_path) as target:
1670+
self.cmdqueue.extend(target.read().splitlines())
1671+
1672+
# Append in an "end of script (eos)" command to cleanup the self._script_dir list
1673+
self.cmdqueue.append('eos')
16521674
except IOError as e:
16531675
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
16541676
return
16551677

1656-
keepstate = Statekeeper(self, ('stdin', 'use_rawinput', 'prompt',
1657-
'continuation_prompt', '_current_script_dir'))
1658-
self.stdin = target
1659-
self.use_rawinput = False
1660-
self.prompt = self.continuation_prompt = ''
1661-
self._current_script_dir = os.path.dirname(expanded_path)
1662-
stop = self._cmdloop()
1663-
self.stdin.close()
1664-
keepstate.restore()
1665-
self.lastcmd = ''
1666-
return stop and (stop != self._STOP_SCRIPT_NO_EXIT)
1678+
self._script_dir.append(os.path.dirname(expanded_path))
16671679

16681680
def do_run(self, arg):
16691681
"""run [arg]: re-runs an earlier command
@@ -1728,16 +1740,6 @@ class TestMyAppCase(Cmd2TestCase):
17281740
runner = unittest.TextTestRunner()
17291741
runner.run(testcase)
17301742

1731-
def _run_commands_at_invocation(self, callargs):
1732-
"""Runs commands provided as arguments on the command line when the application is started.
1733-
1734-
:param callargs: List[str] - list of strings where each string is a command plus its arguments
1735-
:return: bool - True implies the entire application should exit
1736-
"""
1737-
for initial_command in callargs:
1738-
if self.onecmd_plus_hooks(initial_command + '\n'):
1739-
return self._STOP_AND_EXIT
1740-
17411743
def cmdloop(self, intro=None):
17421744
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
17431745
@@ -1749,19 +1751,25 @@ def cmdloop(self, intro=None):
17491751
17501752
:param intro: str - if provided this overrides self.intro and serves as the intro banner printed once at start
17511753
"""
1752-
callargs = None
17531754
if self.allow_cli_args:
17541755
parser = optparse.OptionParser()
17551756
parser.add_option('-t', '--test', dest='test',
17561757
action="store_true",
17571758
help='Test against transcript(s) in FILE (wildcards OK)')
17581759
(callopts, callargs) = parser.parse_args()
1760+
1761+
# If transcript testing was called for, use other arguments as transcript files
17591762
if callopts.test:
17601763
self._transcript_files = callargs
17611764

1765+
# If commands were supplied at invocation, then add them to the command queue
1766+
if callargs:
1767+
self.cmdqueue.extend(callargs)
1768+
17621769
# Always run the preloop first
17631770
self.preloop()
17641771

1772+
# If transcript-based regression testing was requested, then do that instead of the main loop
17651773
if self._transcript_files is not None:
17661774
self.run_transcript_tests(self._transcript_files)
17671775
else:
@@ -1773,14 +1781,8 @@ def cmdloop(self, intro=None):
17731781
if self.intro is not None:
17741782
self.stdout.write(str(self.intro) + "\n")
17751783

1776-
stop = False
1777-
# If allowed, process any commands present as arguments on the command-line, if allowed
1778-
if self.allow_cli_args:
1779-
stop = self._run_commands_at_invocation(callargs)
1780-
1781-
# And then call _cmdloop() if there wasn't something in those causing us to quit
1782-
if not stop:
1783-
self._cmdloop()
1784+
# And then call _cmdloop() to enter the main loop
1785+
self._cmdloop()
17841786

17851787
# Run the postloop() no matter what
17861788
self.postloop()

docs/freefeatures.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,21 @@ Script files
1313
============
1414

1515
Text files can serve as scripts for your ``cmd2``-based
16-
application, with the ``load``, ``save``, and ``edit``
17-
commands.
16+
application, with the ``load``, ``_relative_load``, ``save``, and ``edit`` commands.
17+
18+
Both ASCII and UTF-8 encoded unicode text files are supported.
19+
20+
Simply include one command per line, typed exactly as you would inside a ``cmd2`` application.
1821

1922
.. automethod:: cmd2.Cmd.do_load
2023

24+
.. automethod:: cmd2.Cmd.do__relative_load
25+
2126
.. automethod:: cmd2.Cmd.do_save
2227

2328
.. automethod:: cmd2.Cmd.do_edit
2429

30+
2531
Comments
2632
========
2733

tests/test_cmd2.py

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -317,19 +317,17 @@ def test_base_load(base_app, request):
317317
test_dir = os.path.dirname(request.module.__file__)
318318
filename = os.path.join(test_dir, 'script.txt')
319319

320-
# The way the load command works, we can't directly capture its stdout or stderr
320+
assert base_app.cmdqueue == []
321+
assert base_app._script_dir == []
322+
assert base_app._current_script_dir is None
323+
324+
# Run the load command, which populates the command queue and sets the script directory
321325
run_cmd(base_app, 'load {}'.format(filename))
322326

323-
# But what we can do is check the history to see what commands have been run ...
324-
out = run_cmd(base_app, 'history')
325-
326-
# TODO: Figure out why when we unit test the command this way the commands from the script aren't shown in history
327-
# NOTE: It works correctly when we run it at the command line
328-
expected = normalize("""
329-
-------------------------[1]
330-
load {}
331-
""".format(filename))
332-
assert out == expected
327+
assert base_app.cmdqueue == ['help history', 'eos']
328+
sdir = os.path.dirname(filename)
329+
assert base_app._script_dir == [sdir]
330+
assert base_app._current_script_dir == sdir
333331

334332
def test_load_with_empty_args(base_app, capsys):
335333
# The way the load command works, we can't directly capture its stdout or stderr
@@ -339,6 +337,7 @@ def test_load_with_empty_args(base_app, capsys):
339337
# The load command requires a file path argument, so we should get an error message
340338
expected = normalize("""ERROR: load command requires a file path:\n""")
341339
assert normalize(str(err)) == expected
340+
assert base_app.cmdqueue == []
342341

343342

344343
def test_load_with_nonexistent_file(base_app, capsys):
@@ -349,6 +348,7 @@ def test_load_with_nonexistent_file(base_app, capsys):
349348
# The load command requires a path to an existing file
350349
assert str(err).startswith("ERROR")
351350
assert "does not exist or is not a file" in str(err)
351+
assert base_app.cmdqueue == []
352352

353353

354354
def test_load_with_empty_file(base_app, capsys, request):
@@ -362,6 +362,7 @@ def test_load_with_empty_file(base_app, capsys, request):
362362
# The load command requires non-empty scripts files
363363
assert str(err).startswith("ERROR")
364364
assert "is empty" in str(err)
365+
assert base_app.cmdqueue == []
365366

366367

367368
def test_load_with_binary_file(base_app, capsys, request):
@@ -375,43 +376,48 @@ def test_load_with_binary_file(base_app, capsys, request):
375376
# The load command requires non-empty scripts files
376377
assert str(err).startswith("ERROR")
377378
assert "is not an ASCII or UTF-8 encoded text file" in str(err)
379+
assert base_app.cmdqueue == []
378380

379381

380382
def test_load_with_utf8_file(base_app, capsys, request):
381383
test_dir = os.path.dirname(request.module.__file__)
382384
filename = os.path.join(test_dir, 'scripts', 'utf8.txt')
383385

384-
# The way the load command works, we can't directly capture its stdout or stderr
386+
assert base_app.cmdqueue == []
387+
assert base_app._script_dir == []
388+
assert base_app._current_script_dir is None
389+
390+
# Run the load command, which populates the command queue and sets the script directory
385391
run_cmd(base_app, 'load {}'.format(filename))
386-
out, err = capsys.readouterr()
387392

388-
# TODO Make this test better once shell command is fixed to used cmd2's stdout
389-
assert str(err) == ''
393+
assert base_app.cmdqueue == ['!echo γνωρίζω', 'eos']
394+
sdir = os.path.dirname(filename)
395+
assert base_app._script_dir == [sdir]
396+
assert base_app._current_script_dir == sdir
390397

391398

392399
def test_base_relative_load(base_app, request):
393400
test_dir = os.path.dirname(request.module.__file__)
394401
filename = os.path.join(test_dir, 'script.txt')
395402

396-
# The way the load command works, we can't directly capture its stdout or stderr
397-
run_cmd(base_app, '_relative_load {}'.format(filename))
403+
assert base_app.cmdqueue == []
404+
assert base_app._script_dir == []
405+
assert base_app._current_script_dir is None
398406

399-
# But what we can do is check the history to see what commands have been run ...
400-
out = run_cmd(base_app, 'history')
407+
# Run the load command, which populates the command queue and sets the script directory
408+
run_cmd(base_app, '_relative_load {}'.format(filename))
401409

402-
# TODO: Figure out why when we unit test the command this way the commands from the script aren't shown in history
403-
# NOTE: It works correctly when we run it at the command line
404-
expected = normalize("""
405-
-------------------------[1]
406-
_relative_load {}
407-
""".format(filename))
408-
assert out == expected
410+
assert base_app.cmdqueue == ['help history', 'eos']
411+
sdir = os.path.dirname(filename)
412+
assert base_app._script_dir == [sdir]
413+
assert base_app._current_script_dir == sdir
409414

410415
def test_relative_load_requires_an_argument(base_app, capsys):
411416
run_cmd(base_app, '_relative_load')
412417
out, err = capsys.readouterr()
413418
assert out == ''
414419
assert err.startswith('ERROR: _relative_load command requires a file path:\n')
420+
assert base_app.cmdqueue == []
415421

416422

417423
def test_base_save(base_app):
@@ -1240,3 +1246,19 @@ def test_is_text_file_bad_input(base_app):
12401246
# Test with a directory
12411247
dir_is_valid = base_app.is_text_file('.')
12421248
assert not dir_is_valid
1249+
1250+
1251+
def test_eof(base_app):
1252+
# Only thing to verify is that it returns True
1253+
assert base_app.do_eof('dont care')
1254+
1255+
def test_eos(base_app):
1256+
sdir = 'dummy_dir'
1257+
base_app._script_dir.append(sdir)
1258+
assert len(base_app._script_dir) == 1
1259+
1260+
# Assert that it does NOT return true
1261+
assert not base_app.do_eos('dont care')
1262+
1263+
# And make sure it reduced the length of the script dir list
1264+
assert len(base_app._script_dir) == 0

0 commit comments

Comments
 (0)