Skip to content

Commit ce8da64

Browse files
committed
Added an eos (end of script) hidden command and load now populates the cmdqueue
The load command no longer spawns a nested main loop using _cmdloop(). It now simply adds commands to the cmdqueue. And after adding all commands in the script file, it adds the eos command. The eos command simply pops the most recent script directory from the list of script directories.
1 parent f3968b6 commit ce8da64

File tree

2 files changed

+84
-66
lines changed

2 files changed

+84
-66
lines changed

cmd2.py

Lines changed: 36 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,17 @@ def do_load(self, file_path):
16481659
return
16491660

16501661
try:
1651-
target = open(expanded_path)
1662+
# Add all commands in the script to the command queue
1663+
with open(expanded_path) as target:
1664+
self.cmdqueue.extend(target.read().splitlines())
1665+
1666+
# Append in an "end of script (eos)" command to cleanup the self._script_dir list
1667+
self.cmdqueue.append('eos')
16521668
except IOError as e:
16531669
self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e))
16541670
return
16551671

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)
1672+
self._script_dir.append(os.path.dirname(expanded_path))
16671673

16681674
def do_run(self, arg):
16691675
"""run [arg]: re-runs an earlier command
@@ -1728,16 +1734,6 @@ class TestMyAppCase(Cmd2TestCase):
17281734
runner = unittest.TextTestRunner()
17291735
runner.run(testcase)
17301736

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-
17411737
def cmdloop(self, intro=None):
17421738
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
17431739
@@ -1749,19 +1745,25 @@ def cmdloop(self, intro=None):
17491745
17501746
:param intro: str - if provided this overrides self.intro and serves as the intro banner printed once at start
17511747
"""
1752-
callargs = None
17531748
if self.allow_cli_args:
17541749
parser = optparse.OptionParser()
17551750
parser.add_option('-t', '--test', dest='test',
17561751
action="store_true",
17571752
help='Test against transcript(s) in FILE (wildcards OK)')
17581753
(callopts, callargs) = parser.parse_args()
1754+
1755+
# If transcript testing was called for, use other arguments as transcript files
17591756
if callopts.test:
17601757
self._transcript_files = callargs
17611758

1759+
# If commands were supplied at invocation, then add them to the command queue
1760+
if callargs:
1761+
self.cmdqueue.extend(callargs)
1762+
17621763
# Always run the preloop first
17631764
self.preloop()
17641765

1766+
# If transcript-based regression testing was requested, then do that instead of the main loop
17651767
if self._transcript_files is not None:
17661768
self.run_transcript_tests(self._transcript_files)
17671769
else:
@@ -1773,14 +1775,8 @@ def cmdloop(self, intro=None):
17731775
if self.intro is not None:
17741776
self.stdout.write(str(self.intro) + "\n")
17751777

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()
1778+
# And then call _cmdloop() to enter the main loop
1779+
self._cmdloop()
17841780

17851781
# Run the postloop() no matter what
17861782
self.postloop()

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)