diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2017-07-08 11:09:22 -0400 |
---|---|---|
committer | Todd Leonhardt <todd.leonhardt@gmail.com> | 2017-07-08 11:09:22 -0400 |
commit | ce8da647908a984950fb3b329d2bdd98a99b09e2 (patch) | |
tree | d6fee49d8712e21bd66f85b5c4f2f0f487be5a4f | |
parent | f3968b66ab0c6fc521405db397d75400e8d5477e (diff) | |
download | cmd2-git-ce8da647908a984950fb3b329d2bdd98a99b09e2.tar.gz |
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.
-rwxr-xr-x | cmd2.py | 76 | ||||
-rw-r--r-- | tests/test_cmd2.py | 74 |
2 files changed, 84 insertions, 66 deletions
@@ -416,8 +416,8 @@ class Cmd(cmd.Cmd): allow_cli_args = True # Should arguments passed on the command-line be processed as commands? allow_redirection = True # Should output redirection and pipes be allowed default_to_shell = False # Attempt to run unrecognized commands as shell commands - excludeFromHistory = '''run ru r history histor histo hist his hi h edit edi ed e eof eo'''.split() - exclude_from_help = ['do_eof'] # Commands to exclude from the help menu + excludeFromHistory = '''run ru r history histor histo hist his hi h edit edi ed e eof eo eos'''.split() + exclude_from_help = ['do_eof', 'do_eos'] # Commands to exclude from the help menu reserved_words = [] # Attributes which ARE dynamically settable at runtime @@ -507,8 +507,7 @@ class Cmd(cmd.Cmd): self._temp_filename = None # Codes used for exit conditions - self._STOP_AND_EXIT = True # distinguish end of script file from actual exit - self._STOP_SCRIPT_NO_EXIT = -999 + self._STOP_AND_EXIT = True # cmd convention self._colorcodes = {'bold': {True: '\x1b[1m', False: '\x1b[22m'}, 'cyan': {True: '\x1b[36m', False: '\x1b[39m'}, @@ -519,8 +518,8 @@ class Cmd(cmd.Cmd): 'underline': {True: '\x1b[4m', False: '\x1b[24m'}, 'yellow': {True: '\x1b[33m', False: '\x1b[39m'}} - # Used by load and _relative_load commands - self._current_script_dir = None + # Used load command to store the current script dir as a LIFO queue to support _relative_load command + self._script_dir = [] # ----- Methods related to presenting output to the user ----- @@ -829,7 +828,6 @@ class Cmd(cmd.Cmd): :return: bool - a flag indicating whether the interpretation of commands should stop """ statement = self.parser_manager.parsed(line) - self.lastcmd = statement.parsed.raw funcname = self._func_named(statement.parsed.command) if not funcname: return self._default(statement) @@ -1020,9 +1018,9 @@ class Cmd(cmd.Cmd): # noinspection PyUnusedLocal def do_eof(self, arg): - """Automatically called at end of loading a script or when <Ctrl>-D is pressed.""" + """Called when <Ctrl>-D is pressed.""" # End of script should not exit app, but <Ctrl>-D should. - return self._STOP_SCRIPT_NO_EXIT + return self._STOP_AND_EXIT def do_quit(self, arg): """Exits this application.""" @@ -1591,6 +1589,14 @@ Edited files are run on close if the ``autorun_on_edit`` settable parameter is T except Exception as e: self.perror('Saving {!r} - {}'.format(fname, e), traceback_war=False) + @property + def _current_script_dir(self): + """Accessor to get the current script directory from the _script_dir LIFO queue.""" + if self._script_dir: + return self._script_dir[-1] + else: + return None + def do__relative_load(self, file_path): """Runs commands in script file that is encoded as either ASCII or UTF-8 text. @@ -1616,6 +1622,11 @@ NOTE: This command is intended to only be used within text file scripts. relative_path = os.path.join(self._current_script_dir or '', file_path) self.do_load(relative_path) + def do_eos(self, _): + """Handles cleanup when a script has finished executing.""" + if self._script_dir: + self._script_dir.pop() + def do_load(self, file_path): """Runs commands in script file that is encoded as either ASCII or UTF-8 text. @@ -1648,22 +1659,17 @@ Script should contain one command per line, just like command would be typed in return try: - target = open(expanded_path) + # Add all commands in the script to the command queue + with open(expanded_path) as target: + self.cmdqueue.extend(target.read().splitlines()) + + # Append in an "end of script (eos)" command to cleanup the self._script_dir list + self.cmdqueue.append('eos') except IOError as e: self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e)) return - keepstate = Statekeeper(self, ('stdin', 'use_rawinput', 'prompt', - 'continuation_prompt', '_current_script_dir')) - self.stdin = target - self.use_rawinput = False - self.prompt = self.continuation_prompt = '' - self._current_script_dir = os.path.dirname(expanded_path) - stop = self._cmdloop() - self.stdin.close() - keepstate.restore() - self.lastcmd = '' - return stop and (stop != self._STOP_SCRIPT_NO_EXIT) + self._script_dir.append(os.path.dirname(expanded_path)) def do_run(self, arg): """run [arg]: re-runs an earlier command @@ -1728,16 +1734,6 @@ Script should contain one command per line, just like command would be typed in runner = unittest.TextTestRunner() runner.run(testcase) - def _run_commands_at_invocation(self, callargs): - """Runs commands provided as arguments on the command line when the application is started. - - :param callargs: List[str] - list of strings where each string is a command plus its arguments - :return: bool - True implies the entire application should exit - """ - for initial_command in callargs: - if self.onecmd_plus_hooks(initial_command + '\n'): - return self._STOP_AND_EXIT - def cmdloop(self, intro=None): """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. @@ -1749,19 +1745,25 @@ Script should contain one command per line, just like command would be typed in :param intro: str - if provided this overrides self.intro and serves as the intro banner printed once at start """ - callargs = None if self.allow_cli_args: parser = optparse.OptionParser() parser.add_option('-t', '--test', dest='test', action="store_true", help='Test against transcript(s) in FILE (wildcards OK)') (callopts, callargs) = parser.parse_args() + + # If transcript testing was called for, use other arguments as transcript files if callopts.test: self._transcript_files = callargs + # If commands were supplied at invocation, then add them to the command queue + if callargs: + self.cmdqueue.extend(callargs) + # Always run the preloop first self.preloop() + # If transcript-based regression testing was requested, then do that instead of the main loop if self._transcript_files is not None: self.run_transcript_tests(self._transcript_files) else: @@ -1773,14 +1775,8 @@ Script should contain one command per line, just like command would be typed in if self.intro is not None: self.stdout.write(str(self.intro) + "\n") - stop = False - # If allowed, process any commands present as arguments on the command-line, if allowed - if self.allow_cli_args: - stop = self._run_commands_at_invocation(callargs) - - # And then call _cmdloop() if there wasn't something in those causing us to quit - if not stop: - self._cmdloop() + # And then call _cmdloop() to enter the main loop + self._cmdloop() # Run the postloop() no matter what self.postloop() diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index b75113df..8bb1f52a 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -317,19 +317,17 @@ def test_base_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') - # The way the load command works, we can't directly capture its stdout or stderr + assert base_app.cmdqueue == [] + assert base_app._script_dir == [] + assert base_app._current_script_dir is None + + # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, 'load {}'.format(filename)) - # But what we can do is check the history to see what commands have been run ... - out = run_cmd(base_app, 'history') - - # TODO: Figure out why when we unit test the command this way the commands from the script aren't shown in history - # NOTE: It works correctly when we run it at the command line - expected = normalize(""" --------------------------[1] -load {} -""".format(filename)) - assert out == expected + assert base_app.cmdqueue == ['help history', 'eos'] + sdir = os.path.dirname(filename) + assert base_app._script_dir == [sdir] + assert base_app._current_script_dir == sdir def test_load_with_empty_args(base_app, capsys): # 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): # The load command requires a file path argument, so we should get an error message expected = normalize("""ERROR: load command requires a file path:\n""") assert normalize(str(err)) == expected + assert base_app.cmdqueue == [] def test_load_with_nonexistent_file(base_app, capsys): @@ -349,6 +348,7 @@ def test_load_with_nonexistent_file(base_app, capsys): # The load command requires a path to an existing file assert str(err).startswith("ERROR") assert "does not exist or is not a file" in str(err) + assert base_app.cmdqueue == [] def test_load_with_empty_file(base_app, capsys, request): @@ -362,6 +362,7 @@ def test_load_with_empty_file(base_app, capsys, request): # The load command requires non-empty scripts files assert str(err).startswith("ERROR") assert "is empty" in str(err) + assert base_app.cmdqueue == [] def test_load_with_binary_file(base_app, capsys, request): @@ -375,43 +376,48 @@ def test_load_with_binary_file(base_app, capsys, request): # The load command requires non-empty scripts files assert str(err).startswith("ERROR") assert "is not an ASCII or UTF-8 encoded text file" in str(err) + assert base_app.cmdqueue == [] def test_load_with_utf8_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') - # The way the load command works, we can't directly capture its stdout or stderr + assert base_app.cmdqueue == [] + assert base_app._script_dir == [] + assert base_app._current_script_dir is None + + # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, 'load {}'.format(filename)) - out, err = capsys.readouterr() - # TODO Make this test better once shell command is fixed to used cmd2's stdout - assert str(err) == '' + assert base_app.cmdqueue == ['!echo γνωρίζω', 'eos'] + sdir = os.path.dirname(filename) + assert base_app._script_dir == [sdir] + assert base_app._current_script_dir == sdir def test_base_relative_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') - # The way the load command works, we can't directly capture its stdout or stderr - run_cmd(base_app, '_relative_load {}'.format(filename)) + assert base_app.cmdqueue == [] + assert base_app._script_dir == [] + assert base_app._current_script_dir is None - # But what we can do is check the history to see what commands have been run ... - out = run_cmd(base_app, 'history') + # Run the load command, which populates the command queue and sets the script directory + run_cmd(base_app, '_relative_load {}'.format(filename)) - # TODO: Figure out why when we unit test the command this way the commands from the script aren't shown in history - # NOTE: It works correctly when we run it at the command line - expected = normalize(""" --------------------------[1] -_relative_load {} -""".format(filename)) - assert out == expected + assert base_app.cmdqueue == ['help history', 'eos'] + sdir = os.path.dirname(filename) + assert base_app._script_dir == [sdir] + assert base_app._current_script_dir == sdir def test_relative_load_requires_an_argument(base_app, capsys): run_cmd(base_app, '_relative_load') out, err = capsys.readouterr() assert out == '' assert err.startswith('ERROR: _relative_load command requires a file path:\n') + assert base_app.cmdqueue == [] def test_base_save(base_app): @@ -1240,3 +1246,19 @@ def test_is_text_file_bad_input(base_app): # Test with a directory dir_is_valid = base_app.is_text_file('.') assert not dir_is_valid + + +def test_eof(base_app): + # Only thing to verify is that it returns True + assert base_app.do_eof('dont care') + +def test_eos(base_app): + sdir = 'dummy_dir' + base_app._script_dir.append(sdir) + assert len(base_app._script_dir) == 1 + + # Assert that it does NOT return true + assert not base_app.do_eos('dont care') + + # And make sure it reduced the length of the script dir list + assert len(base_app._script_dir) == 0 |