Repository: python-cmd2/cmd2

Issue:
Stop supporting cmd2 outside of main thread
Much of **cmd2** relies on catching `KeyboardInterrupts`, which means it has to be run in the main thread.  In issue #584, we added a check in `cmdloop()` to prevent installing a signal handler if it is not running in the main thread.

Much of the new additions, especially multiple levels of redirection/piping rely on the user being able to hit Ctrl-C to raise a `KeyboardInterrupt`. I propose we reverse the fix made in #584 since so much of the code can't work outside of the main thread.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11cfe8cb..e973094f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@
     `argparse.Namespace` object they pass to the `do_*` methods. It is stored in an attribute called `__statement__`.
     This can be useful if a command function needs to know the command line for things like logging.
     * Added a `-t` option to the `load` command for automatically generating a transcript based on a script file
+    * When in a *pyscript*, the stdout and stderr streams of shell commands and processes being piped to are now
+    captured and included in the ``CommandResult`` structure.
 * Potentially breaking changes
     * The following commands now write to stderr instead of stdout when printing an error. This will make catching
     errors easier in pyscript.
@@ -24,6 +26,9 @@
         * Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments 
         to ``cmd.Cmd.__init__()`
         * A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them
+    * ``self.pipe_proc`` is now called ``self.cur_pipe_proc_reader`` and is a ``ProcReader`` class.
+    * Shell commands and commands being piped to while in a *pyscript* will function as if their output is going
+    to a pipe and not a tty. This was necessary to be able to capture their output.
 
 ## 0.9.11 (March 13, 2019)
 * Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 34881a3e..a621e459 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -302,12 +302,6 @@ class EmptyStatement(Exception):
 # Contains data about a disabled command which is used to restore its original functions when the command is enabled
 DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])
 
-# Used to restore state after redirection ends
-# redirecting and piping are used to know what needs to be restored
-RedirectionSavedState = utils.namedtuple_with_defaults('RedirectionSavedState',
-                                                       ['redirecting', 'self_stdout', 'sys_stdout',
-                                                        'piping', 'pipe_proc'])
-
 
 class Cmd(cmd.Cmd):
     """An easy but powerful framework for writing line-oriented command interpreters.
@@ -424,8 +418,12 @@ class Cmd(cmd.Cmd):
         # Used load command to store the current script dir as a LIFO queue to support _relative_load command
         self._script_dir = []
 
-        # Used when piping command output to a shell command
-        self.pipe_proc = None
+        # Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
+        self.sigint_protection = utils.ContextFlag()
+
+        # If the current command created a process to pipe to, then this will be a ProcReader object.
+        # Otherwise it will be None. Its used to know when a pipe process can be killed and/or waited upon.
+        self.cur_pipe_proc_reader = None
 
         # Used by complete() for readline tab completion
         self.completion_matches = []
@@ -694,22 +692,12 @@ class Cmd(cmd.Cmd):
                     pager = self.pager
                     if chop:
                         pager = self.pager_chop
-                    self.pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
-                    try:
-                        self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace'))
-                        self.pipe_proc.stdin.close()
-                    except (OSError, KeyboardInterrupt):
-                        pass
 
-                    # Less doesn't respect ^C, but catches it for its own UI purposes (aborting search etc. inside less)
-                    while True:
-                        try:
-                            self.pipe_proc.wait()
-                        except KeyboardInterrupt:
-                            pass
-                        else:
-                            break
-                    self.pipe_proc = None
+                    # Prevent KeyboardInterrupts while in the pager. The pager application will
+                    # still receive the SIGINT since it is in the same process group as us.
+                    with self.sigint_protection:
+                        pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
+                        pipe_proc.communicate(msg_str.encode('utf-8', 'replace'))
                 else:
                     self.decolorized_write(self.stdout, msg_str)
             except BrokenPipeError:
@@ -1654,15 +1642,13 @@ class Cmd(cmd.Cmd):
         :param signum: signal number
         :param frame
         """
+        if self.cur_pipe_proc_reader is not None:
+            # Pass the SIGINT to the current pipe process
+            self.cur_pipe_proc_reader.send_sigint()
 
-        # Save copy of pipe_proc since it could theoretically change while this is running
-        pipe_proc = self.pipe_proc
-
-        if pipe_proc is not None:
-            pipe_proc.terminate()
-
-        # Re-raise a KeyboardInterrupt so other parts of the code can catch it
-        raise KeyboardInterrupt("Got a keyboard interrupt")
+        # Check if we are allowed to re-raise the KeyboardInterrupt
+        if not self.sigint_protection:
+            raise KeyboardInterrupt("Got a keyboard interrupt")
 
     def precmd(self, statement: Statement) -> Statement:
         """Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history.
@@ -1722,44 +1708,56 @@ class Cmd(cmd.Cmd):
             # Keep track of whether or not we were already redirecting before this command
             already_redirecting = self.redirecting
 
-            # Handle any redirection for this command
-            saved_state = self._redirect_output(statement)
-
-            # See if we need to update self.redirecting
-            if not already_redirecting:
-                self.redirecting = saved_state.redirecting or saved_state.piping
+            # This will be a utils.RedirectionSavedState object for the command
+            saved_state = None
 
             try:
-                timestart = datetime.datetime.now()
-                if self._in_py:
-                    self._last_result = None
-
-                # precommand hooks
-                data = plugin.PrecommandData(statement)
-                for func in self._precmd_hooks:
-                    data = func(data)
-                statement = data.statement
-                # call precmd() for compatibility with cmd.Cmd
-                statement = self.precmd(statement)
-
-                # go run the command function
-                stop = self.onecmd(statement)
-
-                # postcommand hooks
-                data = plugin.PostcommandData(stop, statement)
-                for func in self._postcmd_hooks:
-                    data = func(data)
-                # retrieve the final value of stop, ignoring any statement modification from the hooks
-                stop = data.stop
-                # call postcmd() for compatibility with cmd.Cmd
-                stop = self.postcmd(stop, statement)
-
-                if self.timing:
-                    self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart))
+                # Get sigint protection while we set up redirection
+                with self.sigint_protection:
+                    redir_error, saved_state = self._redirect_output(statement)
+                    self.cur_pipe_proc_reader = saved_state.pipe_proc_reader
+
+                # Do not continue if an error occurred while trying to redirect
+                if not redir_error:
+                    # See if we need to update self.redirecting
+                    if not already_redirecting:
+                        self.redirecting = saved_state.redirecting
+
+                    timestart = datetime.datetime.now()
+
+                    # precommand hooks
+                    data = plugin.PrecommandData(statement)
+                    for func in self._precmd_hooks:
+                        data = func(data)
+                    statement = data.statement
+
+                    # call precmd() for compatibility with cmd.Cmd
+                    statement = self.precmd(statement)
+
+                    # go run the command function
+                    stop = self.onecmd(statement)
+
+                    # postcommand hooks
+                    data = plugin.PostcommandData(stop, statement)
+                    for func in self._postcmd_hooks:
+                        data = func(data)
+
+                    # retrieve the final value of stop, ignoring any statement modification from the hooks
+                    stop = data.stop
+
+                    # call postcmd() for compatibility with cmd.Cmd
+                    stop = self.postcmd(stop, statement)
+
+                    if self.timing:
+                        self.pfeedback('Elapsed: {}'.format(datetime.datetime.now() - timestart))
             finally:
-                self._restore_output(statement, saved_state)
-                if not already_redirecting:
-                    self.redirecting = False
+                # Get sigint protection while we restore stuff
+                with self.sigint_protection:
+                    if saved_state is not None:
+                        self._restore_output(statement, saved_state)
+
+                    if not already_redirecting:
+                        self.redirecting = False
 
         except EmptyStatement:
             # don't do anything, but do allow command finalization hooks to run
@@ -1772,9 +1770,10 @@ class Cmd(cmd.Cmd):
     def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) -> bool:
         """Run the command finalization hooks"""
 
-        if not sys.platform.startswith('win'):
-            # Fix those annoying problems that occur with terminal programs like "less" when you pipe to them
-            if self.stdin.isatty():
+        with self.sigint_protection:
+            if not sys.platform.startswith('win') and self.stdout.isatty():
+                # Before the next command runs, fix any terminal problems like those
+                # caused by certain binary characters having been printed to it.
                 import subprocess
                 proc = subprocess.Popen(['stty', 'sane'])
                 proc.communicate()
@@ -1885,46 +1884,69 @@ class Cmd(cmd.Cmd):
             raise EmptyStatement()
         return statement
 
-    def _redirect_output(self, statement: Statement) -> RedirectionSavedState:
+    def _redirect_output(self, statement: Statement) -> Tuple[bool, utils.RedirectionSavedState]:
         """Handles output redirection for >, >>, and |.
 
         :param statement: a parsed statement from the user
-        :return: A RedirectionSavedState object
+        :return: A bool telling if an error occurred and a utils.RedirectionSavedState object
         """
         import io
         import subprocess
 
-        ret_val = RedirectionSavedState(redirecting=False, piping=False)
+        redir_error = False
+
+        # Initialize the saved state
+        saved_state = utils.RedirectionSavedState(self.stdout, sys.stdout, self.cur_pipe_proc_reader)
 
         if not self.allow_redirection:
-            return ret_val
+            return redir_error, saved_state
 
         if statement.pipe_to:
             # Create a pipe with read and write sides
             read_fd, write_fd = os.pipe()
 
-            # Open each side of the pipe and set stdout accordingly
-            pipe_read = io.open(read_fd, 'r')
-            pipe_write = io.open(write_fd, 'w')
+            # Open each side of the pipe
+            subproc_stdin = io.open(read_fd, 'r')
+            new_stdout = io.open(write_fd, 'w')
 
             # We want Popen to raise an exception if it fails to open the process.  Thus we don't set shell to True.
             try:
-                pipe_proc = subprocess.Popen(statement.pipe_to, stdin=pipe_read, stdout=self.stdout)
-                ret_val = RedirectionSavedState(redirecting=True, self_stdout=self.stdout,
-                                                piping=True, pipe_proc=self.pipe_proc)
-                self.stdout = pipe_write
-                self.pipe_proc = pipe_proc
+                # Set options to not forward signals to the pipe process. If a Ctrl-C event occurs,
+                # our sigint handler will forward it only to the most recent pipe process. This makes
+                # sure pipe processes close in the right order (most recent first).
+                if sys.platform == 'win32':
+                    creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
+                    start_new_session = False
+                else:
+                    creationflags = 0
+                    start_new_session = True
+
+                # For any stream that is a StdSim, we will use a pipe so we can capture its output
+                proc = \
+                    subprocess.Popen(statement.pipe_to,
+                                     stdin=subproc_stdin,
+                                     stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout,
+                                     stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
+                                     creationflags=creationflags,
+                                     start_new_session=start_new_session)
+
+                saved_state.redirecting = True
+                saved_state.pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
+                sys.stdout = self.stdout = new_stdout
             except Exception as ex:
-                self.perror('Not piping because - {}'.format(ex), traceback_war=False)
-                pipe_read.close()
-                pipe_write.close()
+                self.perror('Failed to open pipe because - {}'.format(ex), traceback_war=False)
+                subproc_stdin.close()
+                new_stdout.close()
+                redir_error = True
 
         elif statement.output:
             import tempfile
             if (not statement.output_to) and (not self.can_clip):
-                raise EnvironmentError("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable")
+                self.perror("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable",
+                            traceback_war=False)
+                redir_error = True
 
-            if statement.output_to:
+            elif statement.output_to:
                 # going to a file
                 mode = 'w'
                 # statement.output can only contain
@@ -1933,28 +1955,29 @@ class Cmd(cmd.Cmd):
                     mode = 'a'
                 try:
                     new_stdout = open(statement.output_to, mode)
-                    ret_val = RedirectionSavedState(redirecting=True, self_stdout=self.stdout, sys_stdout=sys.stdout)
+                    saved_state.redirecting = True
                     sys.stdout = self.stdout = new_stdout
                 except OSError as ex:
-                    self.perror('Not redirecting because - {}'.format(ex), traceback_war=False)
+                    self.perror('Failed to redirect because - {}'.format(ex), traceback_war=False)
+                    redir_error = True
             else:
                 # going to a paste buffer
                 new_stdout = tempfile.TemporaryFile(mode="w+")
-                ret_val = RedirectionSavedState(redirecting=True, self_stdout=self.stdout, sys_stdout=sys.stdout)
+                saved_state.redirecting = True
                 sys.stdout = self.stdout = new_stdout
+
                 if statement.output == constants.REDIRECTION_APPEND:
                     self.poutput(get_paste_buffer())
 
-        return ret_val
+        return redir_error, saved_state
 
-    def _restore_output(self, statement: Statement, saved_state: RedirectionSavedState) -> None:
+    def _restore_output(self, statement: Statement, saved_state: utils.RedirectionSavedState) -> None:
         """Handles restoring state after output redirection as well as
         the actual pipe operation if present.
 
         :param statement: Statement object which contains the parsed input from the user
         :param saved_state: contains information needed to restore state data
         """
-        # Check if self.stdout was redirected
         if saved_state.redirecting:
             # If we redirected output to the clipboard
             if statement.output and not statement.output_to:
@@ -1966,17 +1989,17 @@ class Cmd(cmd.Cmd):
                 self.stdout.close()
             except BrokenPipeError:
                 pass
-            finally:
-                self.stdout = saved_state.self_stdout
 
-            # Check if sys.stdout was redirected
-            if saved_state.sys_stdout is not None:
-                sys.stdout = saved_state.sys_stdout
+            # Restore the stdout values
+            self.stdout = saved_state.saved_self_stdout
+            sys.stdout = saved_state.saved_sys_stdout
+
+            # Check if we need to wait for the process being piped to
+            if self.cur_pipe_proc_reader is not None:
+                self.cur_pipe_proc_reader.wait()
 
-        # Check if output was being piped to a process
-        if saved_state.piping:
-            self.pipe_proc.communicate()
-            self.pipe_proc = saved_state.pipe_proc
+        # Restore cur_pipe_proc_reader. This always is done, regardless of whether this command redirected.
+        self.cur_pipe_proc_reader = saved_state.saved_pipe_proc_reader
 
     def cmd_func(self, command: str) -> Optional[Callable]:
         """
@@ -2859,7 +2882,8 @@ class Cmd(cmd.Cmd):
             if args.all:
                 self.poutput('\nRead only settings:{}'.format(self.cmdenvironment()))
         else:
-            raise LookupError("Parameter '{}' not supported (type 'set' for list of parameters).".format(param))
+            self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(param),
+                        traceback_war=False)
 
     set_description = ("Set a settable parameter or show current settings of parameters\n"
                        "\n"
@@ -2940,8 +2964,18 @@ class Cmd(cmd.Cmd):
                     tokens[index] = first_char + tokens[index] + first_char
 
         expanded_command = ' '.join(tokens)
-        proc = subprocess.Popen(expanded_command, stdout=self.stdout, shell=True)
-        proc.communicate()
+
+        # Prevent KeyboardInterrupts while in the shell process. The shell process will
+        # still receive the SIGINT since it is in the same process group as us.
+        with self.sigint_protection:
+            # For any stream that is a StdSim, we will use a pipe so we can capture its output
+            proc = subprocess.Popen(expanded_command,
+                                    stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout,
+                                    stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
+                                    shell=True)
+
+            proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
+            proc_reader.wait()
 
     @staticmethod
     def _reset_py_display() -> None:
@@ -3361,49 +3395,52 @@ class Cmd(cmd.Cmd):
                         traceback_war=False)
             return
 
-        # Disable echo while we manually redirect stdout to a StringIO buffer
-        saved_echo = self.echo
-        saved_stdout = self.stdout
-        self.echo = False
-
-        # The problem with supporting regular expressions in transcripts
-        # is that they shouldn't be processed in the command, just the output.
-        # In addition, when we generate a transcript, any slashes in the output
-        # are not really intended to indicate regular expressions, so they should
-        # be escaped.
-        #
-        # We have to jump through some hoops here in order to catch the commands
-        # separately from the output and escape the slashes in the output.
-        transcript = ''
-        for history_item in history:
-            # build the command, complete with prompts. When we replay
-            # the transcript, we look for the prompts to separate
-            # the command from the output
-            first = True
-            command = ''
-            for line in history_item.splitlines():
-                if first:
-                    command += '{}{}\n'.format(self.prompt, line)
-                    first = False
-                else:
-                    command += '{}{}\n'.format(self.continuation_prompt, line)
-            transcript += command
-            # create a new string buffer and set it to stdout to catch the output
-            # of the command
-            membuf = io.StringIO()
-            self.stdout = membuf
-            # then run the command and let the output go into our buffer
-            self.onecmd_plus_hooks(history_item)
-            # rewind the buffer to the beginning
-            membuf.seek(0)
-            # get the output out of the buffer
-            output = membuf.read()
-            # and add the regex-escaped output to the transcript
-            transcript += output.replace('/', r'\/')
-
-        # Restore altered attributes to their original state
-        self.echo = saved_echo
-        self.stdout = saved_stdout
+        try:
+            with self.sigint_protection:
+                # Disable echo while we manually redirect stdout to a StringIO buffer
+                saved_echo = self.echo
+                saved_stdout = self.stdout
+                self.echo = False
+
+            # The problem with supporting regular expressions in transcripts
+            # is that they shouldn't be processed in the command, just the output.
+            # In addition, when we generate a transcript, any slashes in the output
+            # are not really intended to indicate regular expressions, so they should
+            # be escaped.
+            #
+            # We have to jump through some hoops here in order to catch the commands
+            # separately from the output and escape the slashes in the output.
+            transcript = ''
+            for history_item in history:
+                # build the command, complete with prompts. When we replay
+                # the transcript, we look for the prompts to separate
+                # the command from the output
+                first = True
+                command = ''
+                for line in history_item.splitlines():
+                    if first:
+                        command += '{}{}\n'.format(self.prompt, line)
+                        first = False
+                    else:
+                        command += '{}{}\n'.format(self.continuation_prompt, line)
+                transcript += command
+                # create a new string buffer and set it to stdout to catch the output
+                # of the command
+                membuf = io.StringIO()
+                self.stdout = membuf
+                # then run the command and let the output go into our buffer
+                self.onecmd_plus_hooks(history_item)
+                # rewind the buffer to the beginning
+                membuf.seek(0)
+                # get the output out of the buffer
+                output = membuf.read()
+                # and add the regex-escaped output to the transcript
+                transcript += output.replace('/', r'\/')
+        finally:
+            with self.sigint_protection:
+                # Restore altered attributes to their original state
+                self.echo = saved_echo
+                self.stdout = saved_stdout
 
         # finally, we can write the transcript out to the file
         try:
@@ -3814,6 +3851,16 @@ class Cmd(cmd.Cmd):
 
         :param intro: if provided this overrides self.intro and serves as the intro banner printed once at start
         """
+        # cmdloop() expects to be run in the main thread to support extensive use of KeyboardInterrupts throughout the
+        # other built-in functions. You are free to override cmdloop, but much of cmd2's features will be limited.
+        if not threading.current_thread() is threading.main_thread():
+            raise RuntimeError("cmdloop must be run in the main thread")
+
+        # Register a SIGINT signal handler for Ctrl+C
+        import signal
+        original_sigint_handler = signal.getsignal(signal.SIGINT)
+        signal.signal(signal.SIGINT, self.sigint_handler)
+
         if self.allow_cli_args:
             parser = argparse.ArgumentParser()
             parser.add_argument('-t', '--test', action="store_true",
@@ -3828,13 +3875,6 @@ class Cmd(cmd.Cmd):
             if callargs:
                 self.cmdqueue.extend(callargs)
 
-        # Only the main thread is allowed to set a new signal handler in Python, so only attempt if that is the case
-        if threading.current_thread() is threading.main_thread():
-            # Register a SIGINT signal handler for Ctrl+C
-            import signal
-            original_sigint_handler = signal.getsignal(signal.SIGINT)
-            signal.signal(signal.SIGINT, self.sigint_handler)
-
         # Grab terminal lock before the prompt has been drawn by readline
         self.terminal_lock.acquire()
 
@@ -3867,9 +3907,8 @@ class Cmd(cmd.Cmd):
         # This will also zero the lock count in case cmdloop() is called again
         self.terminal_lock.release()
 
-        if threading.current_thread() is threading.main_thread():
-            # Restore the original signal handler
-            signal.signal(signal.SIGINT, original_sigint_handler)
+        # Restore the original signal handler
+        signal.signal(signal.SIGINT, original_sigint_handler)
 
         if self.exit_code is not None:
             sys.exit(self.exit_code)
diff --git a/cmd2/utils.py b/cmd2/utils.py
index f3c29227..d8ac513d 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -1,13 +1,14 @@
-#
 # coding=utf-8
 """Shared utility functions"""
 
 import collections
 import os
 import re
+import subprocess
 import sys
+import threading
 import unicodedata
-from typing import Any, Iterable, List, Optional, Union
+from typing import Any, BinaryIO, Iterable, List, Optional, TextIO, Union
 
 from wcwidth import wcswidth
 
@@ -140,7 +141,6 @@ def which(editor: str) -> Optional[str]:
     :param editor: filename of the editor to check, ie 'notepad.exe' or 'vi'
     :return: a full path or None
     """
-    import subprocess
     try:
         editor_path = subprocess.check_output(['which', editor], stderr=subprocess.STDOUT).strip()
         editor_path = editor_path.decode()
@@ -262,6 +262,32 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
     return sorted(list_to_sort, key=natural_keys)
 
 
+def unquote_redirection_tokens(args: List[str]) -> None:
+    """
+    Unquote redirection tokens in a list of command-line arguments
+    This is used when redirection tokens have to be passed to another command
+    :param args: the command line args
+    """
+    for i, arg in enumerate(args):
+        unquoted_arg = strip_quotes(arg)
+        if unquoted_arg in constants.REDIRECTION_TOKENS:
+            args[i] = unquoted_arg
+
+
+def find_editor() -> str:
+    """Find a reasonable editor to use by default for the system that the cmd2 application is running on."""
+    editor = os.environ.get('EDITOR')
+    if not editor:
+        if sys.platform[:3] == 'win':
+            editor = 'notepad'
+        else:
+            # Favor command-line editors first so we don't leave the terminal to edit
+            for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
+                if which(editor):
+                    break
+    return editor
+
+
 class StdSim(object):
     """
     Class to simulate behavior of sys.stdout or sys.stderr.
@@ -315,7 +341,14 @@ class StdSim(object):
 
     def clear(self) -> None:
         """Clear the internal contents"""
-        self.buffer.byte_buf = b''
+        self.buffer.byte_buf = bytearray()
+
+    def isatty(self) -> bool:
+        """StdSim only considered an interactive stream if `echo` is True and `inner_stream` is a tty."""
+        if self.echo:
+            return self.inner_stream.isatty()
+        else:
+            return False
 
     def __getattr__(self, item: str):
         if item in self.__dict__:
@@ -329,7 +362,7 @@ class ByteBuf(object):
     Used by StdSim to write binary data and stores the actual bytes written
     """
     def __init__(self, std_sim_instance: StdSim) -> None:
-        self.byte_buf = b''
+        self.byte_buf = bytearray()
         self.std_sim_instance = std_sim_instance
 
     def write(self, b: bytes) -> None:
@@ -342,27 +375,140 @@ class ByteBuf(object):
             self.std_sim_instance.inner_stream.buffer.write(b)
 
 
-def unquote_redirection_tokens(args: List[str]) -> None:
+class ProcReader(object):
     """
-    Unquote redirection tokens in a list of command-line arguments
-    This is used when redirection tokens have to be passed to another command
-    :param args: the command line args
+    Used to captured stdout and stderr from a Popen process if any of those were set to subprocess.PIPE.
+    If neither are pipes, then the process will run normally and no output will be captured.
     """
-    for i, arg in enumerate(args):
-        unquoted_arg = strip_quotes(arg)
-        if unquoted_arg in constants.REDIRECTION_TOKENS:
-            args[i] = unquoted_arg
+    def __init__(self, proc: subprocess.Popen, stdout: Union[StdSim, BinaryIO, TextIO],
+                 stderr: Union[StdSim, BinaryIO, TextIO]) -> None:
+        """
+        ProcReader initializer
+        :param proc: the Popen process being read from
+        :param stdout: the stream to write captured stdout
+        :param stderr: the stream to write captured stderr
+        """
+        self._proc = proc
+        self._stdout = stdout
+        self._stderr = stderr
+
+        self._out_thread = threading.Thread(name='out_thread', target=self._reader_thread_func,
+                                            kwargs={'read_stdout': True})
+
+        self._err_thread = threading.Thread(name='out_thread', target=self._reader_thread_func,
+                                            kwargs={'read_stdout': False})
+
+        # Start the reader threads for pipes only
+        if self._proc.stdout is not None:
+            self._out_thread.start()
+        if self._proc.stderr is not None:
+            self._err_thread.start()
+
+    def send_sigint(self) -> None:
+        """Send a SIGINT to the process similar to if <Ctrl>+C were pressed."""
+        import signal
+        if sys.platform.startswith('win'):
+            signal_to_send = signal.CTRL_C_EVENT
+        else:
+            signal_to_send = signal.SIGINT
+        self._proc.send_signal(signal_to_send)
+
+    def terminate(self) -> None:
+        """Terminate the process"""
+        self._proc.terminate()
+
+    def wait(self) -> None:
+        """Wait for the process to finish"""
+        if self._out_thread.is_alive():
+            self._out_thread.join()
+        if self._err_thread.is_alive():
+            self._err_thread.join()
+
+        # Handle case where the process ended before the last read could be done.
+        # This will return None for the streams that weren't pipes.
+        out, err = self._proc.communicate()
+
+        if out:
+            self._write_bytes(self._stdout, out)
+        if err:
+            self._write_bytes(self._stderr, err)
+
+    def _reader_thread_func(self, read_stdout: bool) -> None:
+        """
+        Thread function that reads a stream from the process
+        :param read_stdout: if True, then this thread deals with stdout. Otherwise it deals with stderr.
+        """
+        if read_stdout:
+            read_stream = self._proc.stdout
+            write_stream = self._stdout
+        else:
+            read_stream = self._proc.stderr
+            write_stream = self._stderr
+
+        # The thread should have been started only if this stream was a pipe
+        assert read_stream is not None
+
+        # Run until process completes
+        while self._proc.poll() is None:
+            # noinspection PyUnresolvedReferences
+            available = read_stream.peek()
+            if available:
+                read_stream.read(len(available))
+                self._write_bytes(write_stream, available)
+
+    @staticmethod
+    def _write_bytes(stream: Union[StdSim, BinaryIO, TextIO], to_write: bytes) -> None:
+        """
+        Write bytes to a stream
+        :param stream: the stream being written to
+        :param to_write: the bytes being written
+        """
+        try:
+            if hasattr(stream, 'buffer'):
+                stream.buffer.write(to_write)
+            else:
+                stream.write(to_write)
+        except BrokenPipeError:
+            # This occurs if output is being piped to a process that closed
+            pass
 
 
-def find_editor() -> str:
-    """Find a reasonable editor to use by default for the system that the cmd2 application is running on."""
-    editor = os.environ.get('EDITOR')
-    if not editor:
-        if sys.platform[:3] == 'win':
-            editor = 'notepad'
-        else:
-            # Favor command-line editors first so we don't leave the terminal to edit
-            for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
-                if which(editor):
-                    break
-    return editor
+class ContextFlag(object):
+    """A context manager which is also used as a boolean flag value within the default sigint handler.
+
+    Its main use is as a flag to prevent the SIGINT handler in cmd2 from raising a KeyboardInterrupt
+    while a critical code section has set the flag to True. Because signal handling is always done on the
+    main thread, this class is not thread-safe since there is no need.
+    """
+    def __init__(self) -> None:
+        # When this flag has a positive value, it is considered set.
+        # When it is 0, it is not set. It should never go below 0.
+        self.__count = 0
+
+    def __bool__(self) -> bool:
+        return self.__count > 0
+
+    def __enter__(self) -> None:
+        self.__count += 1
+
+    def __exit__(self, *args) -> None:
+        self.__count -= 1
+        if self.__count < 0:
+            raise ValueError("count has gone below 0")
+
+
+class RedirectionSavedState(object):
+    """Created by each command to store information about their redirection."""
+
+    def __init__(self, self_stdout: Union[StdSim, BinaryIO, TextIO], sys_stdout: Union[StdSim, BinaryIO, TextIO],
+                 pipe_proc_reader: Optional[ProcReader]) -> None:
+        # Used to restore values after the command ends
+        self.saved_self_stdout = self_stdout
+        self.saved_sys_stdout = sys_stdout
+        self.saved_pipe_proc_reader = pipe_proc_reader
+
+        # Tells if the command is redirecting
+        self.redirecting = False
+
+        # If the command created a process to pipe to, then then is its reader
+        self.pipe_proc_reader = None


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index f41afb64..56538718 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,7 @@ Cmd2 unit/functional testing
 Copyright 2016 Federico Ceratto <federico.ceratto@gmail.com>
 Released under MIT license, see LICENSE file
 """
+import sys
 from typing import Optional
 from unittest import mock
 
@@ -13,6 +14,13 @@ from pytest import fixture
 import cmd2
 from cmd2.utils import StdSim
 
+# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
+if sys.version_info < (3, 5):
+    # noinspection PyUnresolvedReferences
+    from contextlib2 import redirect_stdout, redirect_stderr
+else:
+    from contextlib import redirect_stdout, redirect_stderr
+
 # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
 try:
     import gnureadline as readline
@@ -126,19 +134,33 @@ def normalize(block):
 
 
 def run_cmd(app, cmd):
-    """ Clear StdSim buffer, run the command, extract the buffer contents, """
-    app.stdout.clear()
-    app.onecmd_plus_hooks(cmd)
-    out = app.stdout.getvalue()
-    app.stdout.clear()
-    return normalize(out)
+    """ Clear out and err StdSim buffers, run the command, and return out and err """
+    saved_sysout = sys.stdout
+    sys.stdout = app.stdout
+
+    # This will be used to capture app.stdout and sys.stdout
+    copy_cmd_stdout = StdSim(app.stdout)
+
+    # This will be used to capture sys.stderr
+    copy_stderr = StdSim(sys.stderr)
+
+    try:
+        app.stdout = copy_cmd_stdout
+        with redirect_stdout(copy_cmd_stdout):
+            with redirect_stderr(copy_stderr):
+                app.onecmd_plus_hooks(cmd)
+    finally:
+        app.stdout = copy_cmd_stdout.inner_stream
+        sys.stdout = saved_sysout
+
+    out = copy_cmd_stdout.getvalue()
+    err = copy_stderr.getvalue()
+    return normalize(out), normalize(err)
 
 
 @fixture
 def base_app():
-    c = cmd2.Cmd()
-    c.stdout = StdSim(c.stdout)
-    return c
+    return cmd2.Cmd()
 
 
 def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]:
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 74a03e28..d716c68d 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -112,62 +112,60 @@ class ArgparseApp(cmd2.Cmd):
 @pytest.fixture
 def argparse_app():
     app = ArgparseApp()
-    app.stdout = StdSim(app.stdout)
     return app
 
 
-def test_invalid_syntax(argparse_app, capsys):
-    run_cmd(argparse_app, 'speak "')
-    out, err = capsys.readouterr()
-    assert err == "Invalid syntax: No closing quotation\n"
+def test_invalid_syntax(argparse_app):
+    out, err = run_cmd(argparse_app, 'speak "')
+    assert err[0] == "Invalid syntax: No closing quotation"
 
 def test_argparse_basic_command(argparse_app):
-    out = run_cmd(argparse_app, 'say hello')
+    out, err = run_cmd(argparse_app, 'say hello')
     assert out == ['hello']
 
 def test_argparse_quoted_arguments(argparse_app):
-    out = run_cmd(argparse_app, 'say "hello there"')
+    out, err = run_cmd(argparse_app, 'say "hello there"')
     assert out == ['hello there']
 
 def test_argparse_with_list(argparse_app):
-    out = run_cmd(argparse_app, 'speak -s hello world!')
+    out, err = run_cmd(argparse_app, 'speak -s hello world!')
     assert out == ['HELLO WORLD!']
 
 def test_argparse_with_list_and_empty_doc(argparse_app):
-    out = run_cmd(argparse_app, 'speak -s hello world!')
+    out, err = run_cmd(argparse_app, 'speak -s hello world!')
     assert out == ['HELLO WORLD!']
 
 def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app):
-    out = run_cmd(argparse_app, "speak 'This  is a' -s test of the emergency broadcast system!")
+    out, err = run_cmd(argparse_app, "speak 'This  is a' -s test of the emergency broadcast system!")
     assert out == ['THIS  IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!']
 
 def test_argparse_quoted_arguments_multiple(argparse_app):
-    out = run_cmd(argparse_app, 'say "hello  there" "rick & morty"')
+    out, err = run_cmd(argparse_app, 'say "hello  there" "rick & morty"')
     assert out == ['hello  there rick & morty']
 
 def test_argparse_help_docstring(argparse_app):
-    out = run_cmd(argparse_app, 'help say')
+    out, err = run_cmd(argparse_app, 'help say')
     assert out[0].startswith('usage: say')
     assert out[1] == ''
     assert out[2] == 'Repeat what you tell me to.'
 
 def test_argparse_help_description(argparse_app):
-    out = run_cmd(argparse_app, 'help tag')
+    out, err = run_cmd(argparse_app, 'help tag')
     assert out[0].startswith('usage: tag')
     assert out[1] == ''
     assert out[2] == 'create a html tag'
 
 def test_argparse_prog(argparse_app):
-    out = run_cmd(argparse_app, 'help tag')
+    out, err = run_cmd(argparse_app, 'help tag')
     progname = out[0].split(' ')[1]
     assert progname == 'tag'
 
 def test_arglist(argparse_app):
-    out = run_cmd(argparse_app, 'arglist "we  should" get these')
+    out, err = run_cmd(argparse_app, 'arglist "we  should" get these')
     assert out[0] == 'True'
 
 def test_preservelist(argparse_app):
-    out = run_cmd(argparse_app, 'preservelist foo "bar baz"')
+    out, err = run_cmd(argparse_app, 'preservelist foo "bar baz"')
     assert out[0] == "['foo', '\"bar baz\"']"
 
 
@@ -215,39 +213,36 @@ class SubcommandApp(cmd2.Cmd):
 @pytest.fixture
 def subcommand_app():
     app = SubcommandApp()
-    app.stdout = StdSim(app.stdout)
     return app
 
 
 def test_subcommand_foo(subcommand_app):
-    out = run_cmd(subcommand_app, 'base foo -x2 5.0')
+    out, err = run_cmd(subcommand_app, 'base foo -x2 5.0')
     assert out == ['10.0']
 
 
 def test_subcommand_bar(subcommand_app):
-    out = run_cmd(subcommand_app, 'base bar baz')
+    out, err = run_cmd(subcommand_app, 'base bar baz')
     assert out == ['((baz))']
 
-def test_subcommand_invalid(subcommand_app, capsys):
-    run_cmd(subcommand_app, 'base baz')
-    out, err = capsys.readouterr()
-    err = err.splitlines()
+def test_subcommand_invalid(subcommand_app):
+    out, err = run_cmd(subcommand_app, 'base baz')
     assert err[0].startswith('usage: base')
     assert err[1].startswith("base: error: invalid choice: 'baz'")
 
 def test_subcommand_base_help(subcommand_app):
-    out = run_cmd(subcommand_app, 'help base')
+    out, err = run_cmd(subcommand_app, 'help base')
     assert out[0].startswith('usage: base')
     assert out[1] == ''
     assert out[2] == 'Base command help'
 
 def test_subcommand_help(subcommand_app):
-    out = run_cmd(subcommand_app, 'help base foo')
+    out, err = run_cmd(subcommand_app, 'help base foo')
     assert out[0].startswith('usage: base foo')
     assert out[1] == ''
     assert out[2] == 'positional arguments:'
 
 
 def test_subcommand_invalid_help(subcommand_app):
-    out = run_cmd(subcommand_app, 'help base baz')
+    out, err = run_cmd(subcommand_app, 'help base baz')
     assert out[0].startswith('usage: base')
diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py
index 2b9e0458..8aeac6b1 100644
--- a/tests/test_autocompletion.py
+++ b/tests/test_autocompletion.py
@@ -52,12 +52,9 @@ required arguments:
 optional arguments:
   -h, --help            show this help message and exit'''
 
-def test_help_required_group(cmd2_app, capsys):
-    run_cmd(cmd2_app, 'suggest -h')
-    out, err = capsys.readouterr()
-    out1 = normalize(str(out))
-
-    out2 = run_cmd(cmd2_app, 'help suggest')
+def test_help_required_group(cmd2_app):
+    out1, err1 = run_cmd(cmd2_app, 'suggest -h')
+    out2, err2 = run_cmd(cmd2_app, 'help suggest')
 
     assert out1 == out2
     assert out1[0].startswith('Usage: suggest')
@@ -66,12 +63,9 @@ def test_help_required_group(cmd2_app, capsys):
     assert out1 == normalize(SUGGEST_HELP)
 
 
-def test_help_required_group_long(cmd2_app, capsys):
-    run_cmd(cmd2_app, 'media movies add -h')
-    out, err = capsys.readouterr()
-    out1 = normalize(str(out))
-
-    out2 = run_cmd(cmd2_app, 'help media movies add')
+def test_help_required_group_long(cmd2_app):
+    out1, err1 = run_cmd(cmd2_app, 'media movies add -h')
+    out2, err2 = run_cmd(cmd2_app, 'help media movies add')
 
     assert out1 == out2
     assert out1[0].startswith('Usage: media movies add')
@@ -156,11 +150,9 @@ def test_autocomp_flags_narg_max(cmd2_app):
     assert first_match is None
 
 
-def test_autcomp_narg_beyond_max(cmd2_app, capsys):
-    run_cmd(cmd2_app, 'suggest -t movie -d 3 4 5')
-    out, err = capsys.readouterr()
-
-    assert 'Error: unrecognized arguments: 5' in err
+def test_autcomp_narg_beyond_max(cmd2_app):
+    out, err = run_cmd(cmd2_app, 'suggest -t movie -d 3 4 5')
+    assert 'Error: unrecognized arguments: 5' in err[0]
 
 
 def test_autocomp_subcmd_nested(cmd2_app):
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 2137b564..d33477f2 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -29,21 +29,36 @@ from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
     HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
 
 
+@pytest.fixture
+def outsim_app():
+    c = cmd2.Cmd()
+    c.stdout = utils.StdSim(c.stdout)
+    return c
+
 def test_version(base_app):
     assert cmd2.__version__
 
+def test_not_in_main_thread(base_app, capsys):
+    import threading
+    cli_thread = threading.Thread(name='cli_thread', target=base_app.cmdloop)
+
+    cli_thread.start()
+    cli_thread.join()
+    out, err = capsys.readouterr()
+    assert "cmdloop must be run in the main thread" in err
+
 def test_empty_statement(base_app):
-    out = run_cmd(base_app, '')
+    out, err = run_cmd(base_app, '')
     expected = normalize('')
     assert out == expected
 
 def test_base_help(base_app):
-    out = run_cmd(base_app, 'help')
+    out, err = run_cmd(base_app, 'help')
     expected = normalize(BASE_HELP)
     assert out == expected
 
 def test_base_help_verbose(base_app):
-    out = run_cmd(base_app, 'help -v')
+    out, err = run_cmd(base_app, 'help -v')
     expected = normalize(BASE_HELP_VERBOSE)
     assert out == expected
 
@@ -52,32 +67,26 @@ def test_base_help_verbose(base_app):
     help_doc += "\n:param fake param"
     base_app.do_help.__func__.__doc__ = help_doc
 
-    out = run_cmd(base_app, 'help --verbose')
+    out, err = run_cmd(base_app, 'help --verbose')
     assert out == expected
 
-def test_base_argparse_help(base_app, capsys):
+def test_base_argparse_help(base_app):
     # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
-    run_cmd(base_app, 'set -h')
-    out, err = capsys.readouterr()
-    out1 = normalize(str(out))
-
-    out2 = run_cmd(base_app, 'help set')
+    out1, err1 = run_cmd(base_app, 'set -h')
+    out2, err2 = run_cmd(base_app, 'help set')
 
     assert out1 == out2
     assert out1[0].startswith('Usage: set')
     assert out1[1] == ''
     assert out1[2].startswith('Set a settable parameter')
 
-def test_base_invalid_option(base_app, capsys):
-    run_cmd(base_app, 'set -z')
-    out, err = capsys.readouterr()
-    out = normalize(out)
-    err = normalize(err)
+def test_base_invalid_option(base_app):
+    out, err = run_cmd(base_app, 'set -z')
     assert 'Error: unrecognized arguments: -z' in err[0]
     assert out[0] == 'Usage: set [-h] [-a] [-l] [param] [value]'
 
 def test_base_shortcuts(base_app):
-    out = run_cmd(base_app, 'shortcuts')
+    out, err = run_cmd(base_app, 'shortcuts')
     expected = normalize(SHORTCUTS_TXT)
     assert out == expected
 
@@ -85,7 +94,7 @@ def test_base_shortcuts(base_app):
 def test_base_show(base_app):
     # force editor to be 'vim' so test is repeatable across platforms
     base_app.editor = 'vim'
-    out = run_cmd(base_app, 'set')
+    out, err = run_cmd(base_app, 'set')
     expected = normalize(SHOW_TXT)
     assert out == expected
 
@@ -93,14 +102,14 @@ def test_base_show(base_app):
 def test_base_show_long(base_app):
     # force editor to be 'vim' so test is repeatable across platforms
     base_app.editor = 'vim'
-    out = run_cmd(base_app, 'set -l')
+    out, err = run_cmd(base_app, 'set -l')
     expected = normalize(SHOW_LONG)
     assert out == expected
 
 
 def test_base_show_readonly(base_app):
     base_app.editor = 'vim'
-    out = run_cmd(base_app, 'set -a')
+    out, err = run_cmd(base_app, 'set -a')
     expected = normalize(SHOW_TXT + '\nRead only settings:' + """
         Commands may be terminated with: {}
         Arguments at invocation allowed: {}
@@ -149,34 +158,32 @@ def test_cast_problems(capsys):
 
 
 def test_base_set(base_app):
-    out = run_cmd(base_app, 'set quiet True')
+    out, err = run_cmd(base_app, 'set quiet True')
     expected = normalize("""
 quiet - was: False
 now: True
 """)
     assert out == expected
 
-    out = run_cmd(base_app, 'set quiet')
+    out, err = run_cmd(base_app, 'set quiet')
     assert out == ['quiet: True']
 
-def test_set_not_supported(base_app, capsys):
-    run_cmd(base_app, 'set qqq True')
-    out, err = capsys.readouterr()
+def test_set_not_supported(base_app):
+    out, err = run_cmd(base_app, 'set qqq True')
     expected = normalize("""
-EXCEPTION of type 'LookupError' occurred with message: 'Parameter 'qqq' not supported (type 'set' for list of parameters).'
-To enable full traceback, run the following command:  'set debug true'
+Parameter 'qqq' not supported (type 'set' for list of parameters).
 """)
-    assert normalize(str(err)) == expected
+    assert err == expected
 
 def test_set_quiet(base_app):
-    out = run_cmd(base_app, 'set quie True')
+    out, err = run_cmd(base_app, 'set quie True')
     expected = normalize("""
 quiet - was: False
 now: True
 """)
     assert out == expected
 
-    out = run_cmd(base_app, 'set quiet')
+    out, err = run_cmd(base_app, 'set quiet')
     assert out == ['quiet: True']
 
 
@@ -191,11 +198,10 @@ class OnChangeHookApp(cmd2.Cmd):
 @pytest.fixture
 def onchange_app():
     app = OnChangeHookApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_set_onchange_hook(onchange_app):
-    out = run_cmd(onchange_app, 'set quiet True')
+    out, err = run_cmd(onchange_app, 'set quiet True')
     expected = normalize("""
 quiet - was: False
 now: True
@@ -207,61 +213,55 @@ You changed quiet
 def test_base_shell(base_app, monkeypatch):
     m = mock.Mock()
     monkeypatch.setattr("{}.Popen".format('subprocess'), m)
-    out = run_cmd(base_app, 'shell echo a')
+    out, err = run_cmd(base_app, 'shell echo a')
     assert out == []
     assert m.called
 
-def test_base_py(base_app, capsys):
+def test_base_py(base_app):
     # Create a variable and make sure we can see it
-    run_cmd(base_app, 'py qqq=3')
-    out, err = capsys.readouterr()
-    assert out == ''
-    run_cmd(base_app, 'py print(qqq)')
-    out, err = capsys.readouterr()
-    assert out.rstrip() == '3'
+    out, err = run_cmd(base_app, 'py qqq=3')
+    assert not out
+
+    out, err = run_cmd(base_app, 'py print(qqq)')
+    assert out[0].rstrip() == '3'
 
     # Add a more complex statement
-    run_cmd(base_app, 'py print("spaces" + " in this " + "command")')
-    out, err = capsys.readouterr()
-    assert out.rstrip() == 'spaces in this command'
+    out, err = run_cmd(base_app, 'py print("spaces" + " in this " + "command")')
+    assert out[0].rstrip() == 'spaces in this command'
 
     # Set locals_in_py to True and make sure we see self
-    out = run_cmd(base_app, 'set locals_in_py True')
+    out, err = run_cmd(base_app, 'set locals_in_py True')
     assert 'now: True' in out
 
-    run_cmd(base_app, 'py print(self)')
-    out, err = capsys.readouterr()
-    assert 'cmd2.cmd2.Cmd object' in out
+    out, err = run_cmd(base_app, 'py print(self)')
+    assert 'cmd2.cmd2.Cmd object' in out[0]
 
     # Set locals_in_py to False and make sure we can't see self
-    out = run_cmd(base_app, 'set locals_in_py False')
+    out, err = run_cmd(base_app, 'set locals_in_py False')
     assert 'now: False' in out
 
-    run_cmd(base_app, 'py print(self)')
-    out, err = capsys.readouterr()
-    assert "name 'self' is not defined" in err
+    out, err = run_cmd(base_app, 'py print(self)')
+    assert "NameError: name 'self' is not defined" in err
 
 
 @pytest.mark.skipif(sys.platform == 'win32',
                     reason="Unit test doesn't work on win32, but feature does")
-def test_base_run_python_script(base_app, capsys, request):
+def test_base_run_python_script(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     python_script = os.path.join(test_dir, 'script.py')
-    expected = 'This is a python script running ...\n'
+    expected = 'This is a python script running ...'
 
-    run_cmd(base_app, "py run('{}')".format(python_script))
-    out, err = capsys.readouterr()
-    assert out == expected
+    out, err = run_cmd(base_app, "py run('{}')".format(python_script))
+    assert expected in out
 
 
 def test_base_run_pyscript(base_app, capsys, request):
     test_dir = os.path.dirname(request.module.__file__)
     python_script = os.path.join(test_dir, 'script.py')
-    expected = 'This is a python script running ...\n'
+    expected = 'This is a python script running ...'
 
-    run_cmd(base_app, "pyscript {}".format(python_script))
-    out, err = capsys.readouterr()
-    assert out == expected
+    out, err = run_cmd(base_app, "pyscript {}".format(python_script))
+    assert expected in out
 
 def test_recursive_pyscript_not_allowed(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
@@ -272,30 +272,26 @@ def test_recursive_pyscript_not_allowed(base_app, request):
     err = base_app._last_result.stderr
     assert err == expected
 
-def test_pyscript_with_nonexist_file(base_app, capsys):
+def test_pyscript_with_nonexist_file(base_app):
     python_script = 'does_not_exist.py'
-    run_cmd(base_app, "pyscript {}".format(python_script))
-    out, err = capsys.readouterr()
-    assert "Error opening script file" in err
+    out, err = run_cmd(base_app, "pyscript {}".format(python_script))
+    assert "Error opening script file" in err[0]
 
-def test_pyscript_with_exception(base_app, capsys, request):
+def test_pyscript_with_exception(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     python_script = os.path.join(test_dir, 'scripts', 'raises_exception.py')
-    run_cmd(base_app, "pyscript {}".format(python_script))
-    out, err = capsys.readouterr()
-    assert err.startswith('Traceback')
-    assert err.endswith("TypeError: unsupported operand type(s) for +: 'int' and 'str'\n")
+    out, err = run_cmd(base_app, "pyscript {}".format(python_script))
+    assert err[0].startswith('Traceback')
+    assert "TypeError: unsupported operand type(s) for +: 'int' and 'str'" in err[-1]
 
-def test_pyscript_requires_an_argument(base_app, capsys):
-    run_cmd(base_app, "pyscript")
-    out, err = capsys.readouterr()
-    assert "the following arguments are required: script_path" in err
+def test_pyscript_requires_an_argument(base_app):
+    out, err = run_cmd(base_app, "pyscript")
+    assert "the following arguments are required: script_path" in err[0]
 
 
-def test_base_error(base_app, capsys):
-    run_cmd(base_app, 'meow')
-    out, err = capsys.readouterr()
-    assert "is not a recognized command" in err
+def test_base_error(base_app):
+    out, err = run_cmd(base_app, 'meow')
+    assert "is not a recognized command" in err[0]
 
 
 def test_base_load(base_app, request):
@@ -314,62 +310,57 @@ def test_base_load(base_app, request):
     assert base_app._script_dir == [sdir]
     assert base_app._current_script_dir == sdir
 
-def test_load_with_empty_args(base_app, capsys):
+def test_load_with_empty_args(base_app):
     # The way the load command works, we can't directly capture its stdout or stderr
-    run_cmd(base_app, 'load')
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'load')
 
     # The load command requires a file path argument, so we should get an error message
-    assert "the following arguments are required" in str(err)
+    assert "the following arguments are required" in err[0]
     assert base_app.cmdqueue == []
 
 
 def test_load_with_nonexistent_file(base_app, capsys):
     # The way the load command works, we can't directly capture its stdout or stderr
-    run_cmd(base_app, 'load does_not_exist.txt')
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'load does_not_exist.txt')
 
     # The load command requires a path to an existing file
-    assert "does not exist" in str(err)
+    assert "does not exist" in err[0]
     assert base_app.cmdqueue == []
 
-def test_load_with_directory(base_app, capsys, request):
+def test_load_with_directory(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
 
     # The way the load command works, we can't directly capture its stdout or stderr
-    run_cmd(base_app, 'load {}'.format(test_dir))
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'load {}'.format(test_dir))
 
-    assert "is not a file" in str(err)
+    assert "is not a file" in err[0]
     assert base_app.cmdqueue == []
 
-def test_load_with_empty_file(base_app, capsys, request):
+def test_load_with_empty_file(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     filename = os.path.join(test_dir, 'scripts', 'empty.txt')
 
     # The way the load command works, we can't directly capture its stdout or stderr
-    run_cmd(base_app, 'load {}'.format(filename))
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'load {}'.format(filename))
 
     # The load command requires non-empty script files
-    assert "is empty" in str(err)
+    assert "is empty" in err[0]
     assert base_app.cmdqueue == []
 
 
-def test_load_with_binary_file(base_app, capsys, request):
+def test_load_with_binary_file(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     filename = os.path.join(test_dir, 'scripts', 'binary.bin')
 
     # The way the load command works, we can't directly capture its stdout or stderr
-    run_cmd(base_app, 'load {}'.format(filename))
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'load {}'.format(filename))
 
     # The load command requires non-empty scripts files
-    assert "is not an ASCII or UTF-8 encoded text file" in str(err)
+    assert "is not an ASCII or UTF-8 encoded text file" in err[0]
     assert base_app.cmdqueue == []
 
 
-def test_load_with_utf8_file(base_app, capsys, request):
+def test_load_with_utf8_file(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     filename = os.path.join(test_dir, 'scripts', 'utf8.txt')
 
@@ -412,7 +403,8 @@ help
 shortcuts
 _relative_load postcmds.txt
 set colors Never""" % initial_load
-    assert run_cmd(base_app, 'history -s') == normalize(expected)
+    out, err = run_cmd(base_app, 'history -s')
+    assert out == normalize(expected)
 
 
 def test_base_runcmds_plus_hooks(base_app, request):
@@ -434,7 +426,9 @@ help
 shortcuts
 load %s
 set colors Never""" % (prefilepath, postfilepath)
-    assert run_cmd(base_app, 'history -s') == normalize(expected)
+
+    out, err = run_cmd(base_app, 'history -s')
+    assert out == normalize(expected)
 
 
 def test_base_relative_load(base_app, request):
@@ -453,10 +447,9 @@ def test_base_relative_load(base_app, request):
     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 'Error: the following arguments' in err
+def test_relative_load_requires_an_argument(base_app):
+    out, err = run_cmd(base_app, '_relative_load')
+    assert 'Error: the following arguments' in err[0]
     assert base_app.cmdqueue == []
 
 
@@ -539,19 +532,19 @@ def test_feedback_to_output_true(base_app):
         os.remove(filename)
 
 
-def test_feedback_to_output_false(base_app, capsys):
+def test_feedback_to_output_false(base_app):
     base_app.feedback_to_output = False
     base_app.timing = True
     f, filename = tempfile.mkstemp(prefix='feedback_to_output', suffix='.txt')
     os.close(f)
 
     try:
-        run_cmd(base_app, 'help > {}'.format(filename))
-        out, err = capsys.readouterr()
+        out, err = run_cmd(base_app, 'help > {}'.format(filename))
+
         with open(filename) as f:
             content = f.readlines()
         assert not content[-1].startswith('Elapsed: ')
-        assert err.startswith('Elapsed')
+        assert err[0].startswith('Elapsed')
     except:
         raise
     finally:
@@ -565,14 +558,14 @@ def test_disallow_redirection(base_app):
     filename = 'test_allow_redirect.txt'
 
     # Verify output wasn't redirected
-    out = run_cmd(base_app, 'help > {}'.format(filename))
+    out, err = run_cmd(base_app, 'help > {}'.format(filename))
     expected = normalize(BASE_HELP)
     assert out == expected
 
     # Verify that no file got created
     assert not os.path.exists(filename)
 
-def test_pipe_to_shell(base_app, capsys):
+def test_pipe_to_shell(base_app):
     if sys.platform == "win32":
         # Windows
         command = 'help | sort'
@@ -580,27 +573,15 @@ def test_pipe_to_shell(base_app, capsys):
         # Mac and Linux
         # Get help on help and pipe it's output to the input of the word count shell command
         command = 'help help | wc'
-        # # Mac and Linux wc behave the same when piped from shell, but differently when piped stdin from file directly
-        # if sys.platform == 'darwin':
-        #     expected = "1      11      70"
-        # else:
-        #     expected = "1 11 70"
-        # assert out.strip() == expected.strip()
-
-    run_cmd(base_app, command)
-    out, err = capsys.readouterr()
 
-    # Unfortunately with the improved way of piping output to a subprocess, there isn't any good way of getting
-    # access to the output produced by that subprocess within a unit test, but we can verify that no error occurred
-    assert not err
+    out, err = run_cmd(base_app, command)
+    assert out and not err
 
-def test_pipe_to_shell_error(base_app, capsys):
+def test_pipe_to_shell_error(base_app):
     # Try to pipe command output to a shell command that doesn't exist in order to produce an error
-    run_cmd(base_app, 'help | foobarbaz.this_does_not_exist')
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'help | foobarbaz.this_does_not_exist')
     assert not out
-    assert err.startswith("Not piping because")
-
+    assert "Failed to open pipe because" in err[0]
 
 @pytest.mark.skipif(not clipboard.can_clip,
                     reason="Pyperclip could not find a copy/paste mechanism for your system")
@@ -616,38 +597,18 @@ def test_send_to_paste_buffer(base_app):
     assert normalize(cmd2.cmd2.get_paste_buffer()) == expected
 
 
-def test_base_timing(base_app, capsys):
+def test_base_timing(base_app):
     base_app.feedback_to_output = False
-    out = run_cmd(base_app, 'set timing True')
+    out, err = run_cmd(base_app, 'set timing True')
     expected = normalize("""timing - was: False
 now: True
 """)
     assert out == expected
-    out, err = capsys.readouterr()
+
     if sys.platform == 'win32':
-        assert err.startswith('Elapsed: 0:00:00')
+        assert err[0].startswith('Elapsed: 0:00:00')
     else:
-        assert err.startswith('Elapsed: 0:00:00.0')
-
-
-def test_base_debug(base_app, capsys):
-    # Try to set a non-existent parameter with debug set to False by default
-    run_cmd(base_app, 'set does_not_exist 5')
-    out, err = capsys.readouterr()
-    assert err.startswith('EXCEPTION')
-
-    # Set debug true
-    out = run_cmd(base_app, 'set debug True')
-    expected = normalize("""
-debug - was: False
-now: True
-""")
-    assert out == expected
-
-    # Verify that we now see the exception traceback
-    run_cmd(base_app, 'set does_not_exist 5')
-    out, err = capsys.readouterr()
-    assert str(err).startswith('Traceback (most recent call last):')
+        assert err[0].startswith('Elapsed: 0:00:00.0')
 
 
 def _expected_no_editor_error():
@@ -663,16 +624,27 @@ To enable full traceback, run the following command:  'set debug true'
 
     return expected_text
 
-def test_edit_no_editor(base_app, capsys):
+def test_base_debug(base_app):
     # Purposely set the editor to None
     base_app.editor = None
 
     # Make sure we get an exception, but cmd2 handles it
-    run_cmd(base_app, 'edit')
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'edit')
 
     expected = _expected_no_editor_error()
-    assert normalize(str(err)) == expected
+    assert err == expected
+
+    # Set debug true
+    out, err = run_cmd(base_app, 'set debug True')
+    expected = normalize("""
+debug - was: False
+now: True
+""")
+    assert out == expected
+
+    # Verify that we now see the exception traceback
+    out, err = run_cmd(base_app, 'edit')
+    assert err[0].startswith('Traceback (most recent call last):')
 
 def test_edit_file(base_app, request, monkeypatch):
     # Set a fake editor just to make sure we have one.  We aren't really going to call it due to the mock
@@ -731,30 +703,26 @@ def test_base_py_interactive(base_app):
     m.assert_called_once()
 
 
-def test_base_cmdloop_with_queue():
+def test_base_cmdloop_with_queue(outsim_app):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
-    app = cmd2.Cmd()
-    app.use_rawinput = True
+    outsim_app.use_rawinput = True
     intro = 'Hello World, this is an intro ...'
-    app.cmdqueue.append('quit\n')
-    app.stdout = utils.StdSim(app.stdout)
+    outsim_app.cmdqueue.append('quit\n')
 
     # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
     testargs = ["prog"]
     expected = intro + '\n'
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop with custom intro
-        app.cmdloop(intro=intro)
-    out = app.stdout.getvalue()
+        outsim_app.cmdloop(intro=intro)
+    out = outsim_app.stdout.getvalue()
     assert out == expected
 
 
-def test_base_cmdloop_without_queue():
+def test_base_cmdloop_without_queue(outsim_app):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
-    app = cmd2.Cmd()
-    app.use_rawinput = True
-    app.intro = 'Hello World, this is an intro ...'
-    app.stdout = utils.StdSim(app.stdout)
+    outsim_app.use_rawinput = True
+    outsim_app.intro = 'Hello World, this is an intro ...'
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='quit')
@@ -762,21 +730,19 @@ def test_base_cmdloop_without_queue():
 
     # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
     testargs = ["prog"]
-    expected = app.intro + '\n'
+    expected = outsim_app.intro + '\n'
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop
-        app.cmdloop()
-    out = app.stdout.getvalue()
+        outsim_app.cmdloop()
+    out = outsim_app.stdout.getvalue()
     assert out == expected
 
 
-def test_cmdloop_without_rawinput():
+def test_cmdloop_without_rawinput(outsim_app):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
-    app = cmd2.Cmd()
-    app.use_rawinput = False
-    app.echo = False
-    app.intro = 'Hello World, this is an intro ...'
-    app.stdout = utils.StdSim(app.stdout)
+    outsim_app.use_rawinput = False
+    outsim_app.echo = False
+    outsim_app.intro = 'Hello World, this is an intro ...'
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='quit')
@@ -784,11 +750,11 @@ def test_cmdloop_without_rawinput():
 
     # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
     testargs = ["prog"]
-    expected = app.intro + '\n'
+    expected = outsim_app.intro + '\n'
     with mock.patch.object(sys, 'argv', testargs):
         # Run the command loop
-        app.cmdloop()
-    out = app.stdout.getvalue()
+        outsim_app.cmdloop()
+    out = outsim_app.stdout.getvalue()
     assert out == expected
 
 
@@ -806,7 +772,6 @@ class HookFailureApp(cmd2.Cmd):
 @pytest.fixture
 def hook_failure():
     app = HookFailureApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_precmd_hook_success(base_app):
@@ -876,7 +841,7 @@ def test_default_to_shell(base_app, monkeypatch):
     base_app.default_to_shell = True
     m = mock.Mock()
     monkeypatch.setattr("{}.Popen".format('subprocess'), m)
-    out = run_cmd(base_app, line)
+    out, err = run_cmd(base_app, line)
     assert out == []
     assert m.called
 
@@ -930,16 +895,15 @@ class HelpApp(cmd2.Cmd):
 @pytest.fixture
 def help_app():
     app = HelpApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_custom_command_help(help_app):
-    out = run_cmd(help_app, 'help squat')
+    out, err = run_cmd(help_app, 'help squat')
     expected = normalize('This command does diddly squat...')
     assert out == expected
 
 def test_custom_help_menu(help_app):
-    out = run_cmd(help_app, 'help')
+    out, err = run_cmd(help_app, 'help')
     expected = normalize("""
 Documented commands (type help <topic>):
 ========================================
@@ -952,13 +916,12 @@ undoc
 """)
     assert out == expected
 
-def test_help_undocumented(help_app, capsys):
-    run_cmd(help_app, 'help undoc')
-    out, err = capsys.readouterr()
-    assert err.startswith("No help on undoc")
+def test_help_undocumented(help_app):
+    out, err = run_cmd(help_app, 'help undoc')
+    assert err[0].startswith("No help on undoc")
 
 def test_help_overridden_method(help_app):
-    out = run_cmd(help_app, 'help edit')
+    out, err = run_cmd(help_app, 'help edit')
     expected = normalize('This overrides the edit command and does nothing.')
     assert out == expected
 
@@ -998,11 +961,10 @@ class HelpCategoriesApp(cmd2.Cmd):
 @pytest.fixture
 def helpcat_app():
     app = HelpCategoriesApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_help_cat_base(helpcat_app):
-    out = run_cmd(helpcat_app, 'help')
+    out, err = run_cmd(helpcat_app, 'help')
     expected = normalize("""Documented commands (type help <topic>):
 
 Custom Category
@@ -1024,7 +986,7 @@ undoc
     assert out == expected
 
 def test_help_cat_verbose(helpcat_app):
-    out = run_cmd(helpcat_app, 'help --verbose')
+    out, err = run_cmd(helpcat_app, 'help --verbose')
     expected = normalize("""Documented commands (type help <topic>):
 
 Custom Category
@@ -1092,7 +1054,6 @@ class SelectApp(cmd2.Cmd):
 @pytest.fixture
 def select_app():
     app = SelectApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_select_options(select_app):
@@ -1101,7 +1062,7 @@ def test_select_options(select_app):
     builtins.input = m
 
     food = 'bacon'
-    out = run_cmd(select_app, "eat {}".format(food))
+    out, err = run_cmd(select_app, "eat {}".format(food))
     expected = normalize("""
    1. sweet
    2. salty
@@ -1122,7 +1083,7 @@ def test_select_invalid_option_too_big(select_app):
     builtins.input = m
 
     food = 'fish'
-    out = run_cmd(select_app, "eat {}".format(food))
+    out, err = run_cmd(select_app, "eat {}".format(food))
     expected = normalize("""
    1. sweet
    2. salty
@@ -1146,7 +1107,7 @@ def test_select_invalid_option_too_small(select_app):
     builtins.input = m
 
     food = 'fish'
-    out = run_cmd(select_app, "eat {}".format(food))
+    out, err = run_cmd(select_app, "eat {}".format(food))
     expected = normalize("""
    1. sweet
    2. salty
@@ -1167,7 +1128,7 @@ def test_select_list_of_strings(select_app):
     m = mock.MagicMock(name='input', return_value='2')
     builtins.input = m
 
-    out = run_cmd(select_app, "study")
+    out, err = run_cmd(select_app, "study")
     expected = normalize("""
    1. math
    2. science
@@ -1185,7 +1146,7 @@ def test_select_list_of_tuples(select_app):
     m = mock.MagicMock(name='input', return_value='2')
     builtins.input = m
 
-    out = run_cmd(select_app, "procrastinate")
+    out, err = run_cmd(select_app, "procrastinate")
     expected = normalize("""
    1. Netflix
    2. WebSurfing
@@ -1204,7 +1165,7 @@ def test_select_uneven_list_of_tuples(select_app):
     m = mock.MagicMock(name='input', return_value='2')
     builtins.input = m
 
-    out = run_cmd(select_app, "play")
+    out, err = run_cmd(select_app, "play")
     expected = normalize("""
    1. Electric Guitar
    2. Drums
@@ -1280,7 +1241,6 @@ class MultilineApp(cmd2.Cmd):
 @pytest.fixture
 def multiline_app():
     app = MultilineApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_multiline_complete_empty_statement_raises_exception(multiline_app):
@@ -1347,7 +1307,6 @@ class CommandResultApp(cmd2.Cmd):
 @pytest.fixture
 def commandresult_app():
     app = CommandResultApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_commandresult_truthy(commandresult_app):
@@ -1507,7 +1466,7 @@ def test_pseudo_raw_input_piped_rawinput_false_echo_false(capsys):
     assert firstline.startswith('colors:')
     assert not '{}{}'.format(app.prompt, command) in out
 
-#
+
 # other input tests
 def test_raw_input(base_app):
     base_app.use_raw_input = True
@@ -1544,50 +1503,49 @@ def test_empty_stdin_input():
     line = app.pseudo_raw_input('(cmd2)')
     assert line == 'eof'
 
-
-def test_poutput_string(base_app):
+def test_poutput_string(outsim_app):
     msg = 'This is a test'
-    base_app.poutput(msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.poutput(msg)
+    out = outsim_app.stdout.getvalue()
     expected = msg + '\n'
     assert out == expected
 
-def test_poutput_zero(base_app):
+def test_poutput_zero(outsim_app):
     msg = 0
-    base_app.poutput(msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.poutput(msg)
+    out = outsim_app.stdout.getvalue()
     expected = str(msg) + '\n'
     assert out == expected
 
-def test_poutput_empty_string(base_app):
+def test_poutput_empty_string(outsim_app):
     msg = ''
-    base_app.poutput(msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.poutput(msg)
+    out = outsim_app.stdout.getvalue()
     expected = msg
     assert out == expected
 
-def test_poutput_none(base_app):
+def test_poutput_none(outsim_app):
     msg = None
-    base_app.poutput(msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.poutput(msg)
+    out = outsim_app.stdout.getvalue()
     expected = ''
     assert out == expected
 
-def test_poutput_color_always(base_app):
+def test_poutput_color_always(outsim_app):
     msg = 'Hello World'
     color = Fore.CYAN
-    base_app.colors = 'Always'
-    base_app.poutput(msg, color=color)
-    out = base_app.stdout.getvalue()
+    outsim_app.colors = 'Always'
+    outsim_app.poutput(msg, color=color)
+    out = outsim_app.stdout.getvalue()
     expected = color + msg + '\n' + Fore.RESET
     assert out == expected
 
-def test_poutput_color_never(base_app):
+def test_poutput_color_never(outsim_app):
     msg = 'Hello World'
     color = Fore.CYAN
-    base_app.colors = 'Never'
-    base_app.poutput(msg, color=color)
-    out = base_app.stdout.getvalue()
+    outsim_app.colors = 'Never'
+    outsim_app.poutput(msg, color=color)
+    out = outsim_app.stdout.getvalue()
     expected = msg + '\n'
     assert out == expected
 
@@ -1622,88 +1580,82 @@ def test_get_macro_names(base_app):
 def test_get_settable_names(base_app):
     assert sorted(base_app.get_settable_names()) == sorted(base_app.settable.keys())
 
-def test_alias_no_subcommand(base_app, capsys):
-    out = run_cmd(base_app, 'alias')
+def test_alias_no_subcommand(base_app):
+    out, err = run_cmd(base_app, 'alias')
     assert "Usage: alias [-h]" in out[0]
 
-def test_alias_create(base_app, capsys):
+def test_alias_create(base_app):
     # Create the alias
-    out = run_cmd(base_app, 'alias create fake pyscript')
+    out, err = run_cmd(base_app, 'alias create fake pyscript')
     assert out == normalize("Alias 'fake' created")
 
     # Use the alias
-    run_cmd(base_app, 'fake')
-    out, err = capsys.readouterr()
-    assert "the following arguments are required: script_path" in err
+    out, err = run_cmd(base_app, 'fake')
+    assert "the following arguments are required: script_path" in err[0]
 
     # See a list of aliases
-    out = run_cmd(base_app, 'alias list')
+    out, err = run_cmd(base_app, 'alias list')
     assert out == normalize('alias create fake pyscript')
 
     # Look up the new alias
-    out = run_cmd(base_app, 'alias list fake')
+    out, err = run_cmd(base_app, 'alias list fake')
     assert out == normalize('alias create fake pyscript')
 
-def test_alias_create_with_quoted_value(base_app, capsys):
+def test_alias_create_with_quoted_value(base_app):
     """Demonstrate that quotes in alias value will be preserved (except for redirectors)"""
 
     # Create the alias
-    out = run_cmd(base_app, 'alias create fake help ">" "out file.txt"')
+    out, err = run_cmd(base_app, 'alias create fake help ">" "out file.txt"')
     assert out == normalize("Alias 'fake' created")
 
     # Look up the new alias (Only the redirector should be unquoted)
-    out = run_cmd(base_app, 'alias list fake')
+    out, err = run_cmd(base_app, 'alias list fake')
     assert out == normalize('alias create fake help > "out file.txt"')
 
 @pytest.mark.parametrize('alias_name', invalid_command_name)
 def test_alias_create_invalid_name(base_app, alias_name, capsys):
-    run_cmd(base_app, 'alias create {} help'.format(alias_name))
-    out, err = capsys.readouterr()
-    assert "Invalid alias name" in err
+    out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name))
+    assert "Invalid alias name" in err[0]
 
-def test_alias_create_with_macro_name(base_app, capsys):
+def test_alias_create_with_macro_name(base_app):
     macro = "my_macro"
     run_cmd(base_app, 'macro create {} help'.format(macro))
-    run_cmd(base_app, 'alias create {} help'.format(macro))
-    out, err = capsys.readouterr()
-    assert "Alias cannot have the same name as a macro" in err
+    out, err = run_cmd(base_app, 'alias create {} help'.format(macro))
+    assert "Alias cannot have the same name as a macro" in err[0]
 
-def test_alias_that_resolves_into_comment(base_app, capsys):
+def test_alias_that_resolves_into_comment(base_app):
     # Create the alias
-    out = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
+    out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
     assert out == normalize("Alias 'fake' created")
 
     # Use the alias
-    run_cmd(base_app, 'fake')
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'fake')
     assert not out
     assert not err
 
-def test_alias_list_invalid_alias(base_app, capsys):
+def test_alias_list_invalid_alias(base_app):
     # Look up invalid alias
-    out = run_cmd(base_app, 'alias list invalid')
-    out, err = capsys.readouterr()
-    assert "Alias 'invalid' not found" in err
+    out, err = run_cmd(base_app, 'alias list invalid')
+    assert "Alias 'invalid' not found" in err[0]
 
 def test_alias_delete(base_app):
     # Create an alias
     run_cmd(base_app, 'alias create fake pyscript')
 
     # Delete the alias
-    out = run_cmd(base_app, 'alias delete fake')
+    out, err = run_cmd(base_app, 'alias delete fake')
     assert out == normalize("Alias 'fake' deleted")
 
 def test_alias_delete_all(base_app):
-    out = run_cmd(base_app, 'alias delete --all')
+    out, err = run_cmd(base_app, 'alias delete --all')
     assert out == normalize("All aliases deleted")
 
-def test_alias_delete_non_existing(base_app, capsys):
-    run_cmd(base_app, 'alias delete fake')
-    out, err = capsys.readouterr()
-    assert "Alias 'fake' does not exist" in err
+def test_alias_delete_non_existing(base_app):
+    out, err = run_cmd(base_app, 'alias delete fake')
+    assert "Alias 'fake' does not exist" in err[0]
 
-def test_alias_delete_no_name(base_app, capsys):
-    out = run_cmd(base_app, 'alias delete')
+def test_alias_delete_no_name(base_app):
+    out, err = run_cmd(base_app, 'alias delete')
     assert "Usage: alias delete" in out[0]
 
 def test_multiple_aliases(base_app):
@@ -1711,167 +1663,154 @@ def test_multiple_aliases(base_app):
     alias2 = 'h2'
     run_cmd(base_app, 'alias create {} help'.format(alias1))
     run_cmd(base_app, 'alias create {} help -v'.format(alias2))
-    out = run_cmd(base_app, alias1)
+    out, err = run_cmd(base_app, alias1)
     expected = normalize(BASE_HELP)
     assert out == expected
 
-    out = run_cmd(base_app, alias2)
+    out, err = run_cmd(base_app, alias2)
     expected = normalize(BASE_HELP_VERBOSE)
     assert out == expected
 
-def test_macro_no_subcommand(base_app, capsys):
-    out = run_cmd(base_app, 'macro')
+def test_macro_no_subcommand(base_app):
+    out, err = run_cmd(base_app, 'macro')
     assert "Usage: macro [-h]" in out[0]
 
-def test_macro_create(base_app, capsys):
+def test_macro_create(base_app):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake pyscript')
+    out, err = run_cmd(base_app, 'macro create fake pyscript')
     assert out == normalize("Macro 'fake' created")
 
     # Use the macro
-    run_cmd(base_app, 'fake')
-    out, err = capsys.readouterr()
-    assert "the following arguments are required: script_path" in err
+    out, err = run_cmd(base_app, 'fake')
+    assert "the following arguments are required: script_path" in err[0]
 
     # See a list of macros
-    out = run_cmd(base_app, 'macro list')
+    out, err = run_cmd(base_app, 'macro list')
     assert out == normalize('macro create fake pyscript')
 
     # Look up the new macro
-    out = run_cmd(base_app, 'macro list fake')
+    out, err = run_cmd(base_app, 'macro list fake')
     assert out == normalize('macro create fake pyscript')
 
-def test_macro_create_with_quoted_value(base_app, capsys):
+def test_macro_create_with_quoted_value(base_app):
     """Demonstrate that quotes in macro value will be preserved (except for redirectors)"""
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake help ">" "out file.txt"')
+    out, err = run_cmd(base_app, 'macro create fake help ">" "out file.txt"')
     assert out == normalize("Macro 'fake' created")
 
     # Look up the new macro (Only the redirector should be unquoted)
-    out = run_cmd(base_app, 'macro list fake')
+    out, err = run_cmd(base_app, 'macro list fake')
     assert out == normalize('macro create fake help > "out file.txt"')
 
 @pytest.mark.parametrize('macro_name', invalid_command_name)
-def test_macro_create_invalid_name(base_app, macro_name, capsys):
-    run_cmd(base_app, 'macro create {} help'.format(macro_name))
-    out, err = capsys.readouterr()
-    assert "Invalid macro name" in err
+def test_macro_create_invalid_name(base_app, macro_name):
+    out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name))
+    assert "Invalid macro name" in err[0]
 
-def test_macro_create_with_alias_name(base_app, capsys):
+def test_macro_create_with_alias_name(base_app):
     macro = "my_macro"
     run_cmd(base_app, 'alias create {} help'.format(macro))
-    run_cmd(base_app, 'macro create {} help'.format(macro))
-    out, err = capsys.readouterr()
-    assert "Macro cannot have the same name as an alias" in err
+    out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
+    assert "Macro cannot have the same name as an alias" in err[0]
 
-def test_macro_create_with_command_name(base_app, capsys):
+def test_macro_create_with_command_name(base_app):
     macro = "my_macro"
-    run_cmd(base_app, 'macro create help stuff')
-    out, err = capsys.readouterr()
-    assert "Macro cannot have the same name as a command" in err
+    out, err = run_cmd(base_app, 'macro create help stuff')
+    assert "Macro cannot have the same name as a command" in err[0]
 
-def test_macro_create_with_args(base_app, capsys):
+def test_macro_create_with_args(base_app):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake {1} {2}')
+    out, err = run_cmd(base_app, 'macro create fake {1} {2}')
     assert out == normalize("Macro 'fake' created")
 
     # Run the macro
-    out = run_cmd(base_app, 'fake help -v')
+    out, err = run_cmd(base_app, 'fake help -v')
     expected = normalize(BASE_HELP_VERBOSE)
     assert out == expected
 
-def test_macro_create_with_escaped_args(base_app, capsys):
+def test_macro_create_with_escaped_args(base_app):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake help {{1}}')
+    out, err = run_cmd(base_app, 'macro create fake help {{1}}')
     assert out == normalize("Macro 'fake' created")
 
     # Run the macro
-    run_cmd(base_app, 'fake')
-    out, err = capsys.readouterr()
-    assert err.startswith('No help on {1}')
+    out, err = run_cmd(base_app, 'fake')
+    assert err[0].startswith('No help on {1}')
 
-def test_macro_usage_with_missing_args(base_app, capsys):
+def test_macro_usage_with_missing_args(base_app):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake help {1} {2}')
+    out, err = run_cmd(base_app, 'macro create fake help {1} {2}')
     assert out == normalize("Macro 'fake' created")
 
     # Run the macro
-    run_cmd(base_app, 'fake arg1')
-    out, err = capsys.readouterr()
-    assert "expects at least 2 argument(s)" in err
+    out, err = run_cmd(base_app, 'fake arg1')
+    assert "expects at least 2 argument(s)" in err[0]
 
-def test_macro_usage_with_exta_args(base_app, capsys):
+def test_macro_usage_with_exta_args(base_app):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake help {1}')
+    out, err = run_cmd(base_app, 'macro create fake help {1}')
     assert out == normalize("Macro 'fake' created")
 
     # Run the macro
-    out = run_cmd(base_app, 'fake alias create')
+    out, err = run_cmd(base_app, 'fake alias create')
     assert "Usage: alias create" in out[0]
 
-def test_macro_create_with_missing_arg_nums(base_app, capsys):
+def test_macro_create_with_missing_arg_nums(base_app):
     # Create the macro
-    run_cmd(base_app, 'macro create fake help {1} {3}')
-    out, err = capsys.readouterr()
-    assert "Not all numbers between 1 and 3" in err
+    out, err = run_cmd(base_app, 'macro create fake help {1} {3}')
+    assert "Not all numbers between 1 and 3" in err[0]
 
-def test_macro_create_with_invalid_arg_num(base_app, capsys):
+def test_macro_create_with_invalid_arg_num(base_app):
     # Create the macro
-    run_cmd(base_app, 'macro create fake help {1} {-1} {0}')
-    out, err = capsys.readouterr()
-    assert "Argument numbers must be greater than 0" in err
+    out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}')
+    assert "Argument numbers must be greater than 0" in err[0]
 
-def test_macro_create_with_unicode_numbered_arg(base_app, capsys):
+def test_macro_create_with_unicode_numbered_arg(base_app):
     # Create the macro expecting 1 argument
-    out = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}')
+    out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}')
     assert out == normalize("Macro 'fake' created")
 
     # Run the macro
-    out = run_cmd(base_app, 'fake')
-    out, err = capsys.readouterr()
-    assert "expects at least 1 argument(s)" in err
+    out, err = run_cmd(base_app, 'fake')
+    assert "expects at least 1 argument(s)" in err[0]
 
-def test_macro_create_with_missing_unicode_arg_nums(base_app, capsys):
-    run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}')
-    out, err = capsys.readouterr()
-    assert "Not all numbers between 1 and 3" in err
+def test_macro_create_with_missing_unicode_arg_nums(base_app):
+    out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}')
+    assert "Not all numbers between 1 and 3" in err[0]
 
-def test_macro_that_resolves_into_comment(base_app, capsys):
+def test_macro_that_resolves_into_comment(base_app):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake {1} blah blah')
+    out, err = run_cmd(base_app, 'macro create fake {1} blah blah')
     assert out == normalize("Macro 'fake' created")
 
     # Use the macro
-    run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
-    out, err = capsys.readouterr()
+    out, err = run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
     assert not out
     assert not err
 
-def test_macro_list_invalid_macro(base_app, capsys):
+def test_macro_list_invalid_macro(base_app):
     # Look up invalid macro
-    run_cmd(base_app, 'macro list invalid')
-    out, err = capsys.readouterr()
-    assert "Macro 'invalid' not found" in err
+    out, err = run_cmd(base_app, 'macro list invalid')
+    assert "Macro 'invalid' not found" in err[0]
 
 def test_macro_delete(base_app):
     # Create an macro
     run_cmd(base_app, 'macro create fake pyscript')
 
     # Delete the macro
-    out = run_cmd(base_app, 'macro delete fake')
+    out, err = run_cmd(base_app, 'macro delete fake')
     assert out == normalize("Macro 'fake' deleted")
 
 def test_macro_delete_all(base_app):
-    out = run_cmd(base_app, 'macro delete --all')
+    out, err = run_cmd(base_app, 'macro delete --all')
     assert out == normalize("All macros deleted")
 
-def test_macro_delete_non_existing(base_app, capsys):
-    run_cmd(base_app, 'macro delete fake')
-    out, err = capsys.readouterr()
-    assert "Macro 'fake' does not exist" in err
+def test_macro_delete_non_existing(base_app):
+    out, err = run_cmd(base_app, 'macro delete fake')
+    assert "Macro 'fake' does not exist" in err[0]
 
-def test_macro_delete_no_name(base_app, capsys):
-    out = run_cmd(base_app, 'macro delete')
+def test_macro_delete_no_name(base_app):
+    out, err = run_cmd(base_app, 'macro delete')
     assert "Usage: macro delete" in out[0]
 
 def test_multiple_macros(base_app):
@@ -1879,15 +1818,15 @@ def test_multiple_macros(base_app):
     macro2 = 'h2'
     run_cmd(base_app, 'macro create {} help'.format(macro1))
     run_cmd(base_app, 'macro create {} help -v'.format(macro2))
-    out = run_cmd(base_app, macro1)
+    out, err = run_cmd(base_app, macro1)
     expected = normalize(BASE_HELP)
     assert out == expected
 
-    out = run_cmd(base_app, macro2)
+    out, err = run_cmd(base_app, macro2)
     expected = normalize(BASE_HELP_VERBOSE)
     assert out == expected
 
-def test_nonexistent_macro(base_app, capsys):
+def test_nonexistent_macro(base_app):
     from cmd2.parsing import StatementParser
     exception = None
 
@@ -1899,29 +1838,29 @@ def test_nonexistent_macro(base_app, capsys):
     assert exception is not None
 
 
-def test_ppaged(base_app):
+def test_ppaged(outsim_app):
     msg = 'testing...'
     end = '\n'
-    base_app.ppaged(msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.ppaged(msg)
+    out = outsim_app.stdout.getvalue()
     assert out == msg + end
 
-def test_ppaged_strips_color_when_redirecting(base_app):
+def test_ppaged_strips_color_when_redirecting(outsim_app):
     msg = 'testing...'
     end = '\n'
-    base_app.colors = cmd2.constants.COLORS_TERMINAL
-    base_app.redirecting = True
-    base_app.ppaged(Fore.RED + msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.colors = cmd2.constants.COLORS_TERMINAL
+    outsim_app.redirecting = True
+    outsim_app.ppaged(Fore.RED + msg)
+    out = outsim_app.stdout.getvalue()
     assert out == msg + end
 
-def test_ppaged_strips_color_when_redirecting_if_always(base_app):
+def test_ppaged_strips_color_when_redirecting_if_always(outsim_app):
     msg = 'testing...'
     end = '\n'
-    base_app.colors = cmd2.constants.COLORS_ALWAYS
-    base_app.redirecting = True
-    base_app.ppaged(Fore.RED + msg)
-    out = base_app.stdout.getvalue()
+    outsim_app.colors = cmd2.constants.COLORS_ALWAYS
+    outsim_app.redirecting = True
+    outsim_app.ppaged(Fore.RED + msg)
+    out = outsim_app.stdout.getvalue()
     assert out == Fore.RED + msg + end
 
 # we override cmd.parseline() so we always get consistent
@@ -1943,17 +1882,17 @@ def test_parseline(base_app):
     assert line == statement.strip()
 
 
-def test_onecmd_raw_str_continue(base_app):
+def test_onecmd_raw_str_continue(outsim_app):
     line = "help"
-    stop = base_app.onecmd(line)
-    out = base_app.stdout.getvalue()
+    stop = outsim_app.onecmd(line)
+    out = outsim_app.stdout.getvalue()
     assert not stop
     assert normalize(out) == normalize(BASE_HELP)
 
-def test_onecmd_raw_str_quit(base_app):
+def test_onecmd_raw_str_quit(outsim_app):
     line = "quit"
-    stop = base_app.onecmd(line)
-    out = base_app.stdout.getvalue()
+    stop = outsim_app.onecmd(line)
+    out = outsim_app.stdout.getvalue()
     assert stop
     assert out == ''
 
@@ -2004,13 +1943,13 @@ Usage:  exit [exit_code]
 @pytest.fixture
 def exit_code_repl():
     app = ReplWithExitCode()
+    app.stdout = utils.StdSim(app.stdout)
     return app
 
 def test_exit_code_default(exit_code_repl):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
     app = exit_code_repl
     app.use_rawinput = True
-    app.stdout = utils.StdSim(app.stdout)
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='exit')
@@ -2029,7 +1968,6 @@ def test_exit_code_nonzero(exit_code_repl):
     # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test
     app = exit_code_repl
     app.use_rawinput = True
-    app.stdout = utils.StdSim(app.stdout)
 
     # Mock out the input call so we don't actually wait for a user's response on stdin
     m = mock.MagicMock(name='input', return_value='exit 23')
@@ -2196,31 +2134,26 @@ class DisableCommandsApp(cmd2.Cmd):
 @pytest.fixture
 def disable_commands_app():
     app = DisableCommandsApp()
-    app.stdout = utils.StdSim(app.stdout)
     return app
 
 
-def test_disable_and_enable_category(disable_commands_app, capsys):
+def test_disable_and_enable_category(disable_commands_app):
     # Disable the category
     message_to_print = 'These commands are currently disabled'
     disable_commands_app.disable_category(disable_commands_app.category_name, message_to_print)
 
     # Make sure all the commands and help on those commands displays the message
-    run_cmd(disable_commands_app, 'has_help_func')
-    out, err = capsys.readouterr()
-    assert err.startswith(message_to_print)
+    out, err = run_cmd(disable_commands_app, 'has_help_func')
+    assert err[0].startswith(message_to_print)
 
-    run_cmd(disable_commands_app, 'help has_help_func')
-    out, err = capsys.readouterr()
-    assert err.startswith(message_to_print)
+    out, err = run_cmd(disable_commands_app, 'help has_help_func')
+    assert err[0].startswith(message_to_print)
 
-    run_cmd(disable_commands_app, 'has_no_help_func')
-    out, err = capsys.readouterr()
-    assert err.startswith(message_to_print)
+    out, err = run_cmd(disable_commands_app, 'has_no_help_func')
+    assert err[0].startswith(message_to_print)
 
-    run_cmd(disable_commands_app, 'help has_no_help_func')
-    out, err = capsys.readouterr()
-    assert err.startswith(message_to_print)
+    out, err = run_cmd(disable_commands_app, 'help has_no_help_func')
+    assert err[0].startswith(message_to_print)
 
     visible_commands = disable_commands_app.get_visible_commands()
     assert 'has_help_func' not in visible_commands
@@ -2230,17 +2163,17 @@ def test_disable_and_enable_category(disable_commands_app, capsys):
     disable_commands_app.enable_category(disable_commands_app.category_name)
 
     # Make sure all the commands and help on those commands are restored
-    out = run_cmd(disable_commands_app, 'has_help_func')
-    assert out == ["The real has_help_func"]
+    out, err = run_cmd(disable_commands_app, 'has_help_func')
+    assert out[0] == "The real has_help_func"
 
-    out = run_cmd(disable_commands_app, 'help has_help_func')
-    assert out == ["Help for has_help_func"]
+    out, err = run_cmd(disable_commands_app, 'help has_help_func')
+    assert out[0] == "Help for has_help_func"
 
-    out = run_cmd(disable_commands_app, 'has_no_help_func')
-    assert out == ["The real has_no_help_func"]
+    out, err = run_cmd(disable_commands_app, 'has_no_help_func')
+    assert out[0] == "The real has_no_help_func"
 
-    out = run_cmd(disable_commands_app, 'help has_no_help_func')
-    assert out == ["Help for has_no_help_func"]
+    out, err = run_cmd(disable_commands_app, 'help has_no_help_func')
+    assert out[0] == "Help for has_no_help_func"
 
     visible_commands = disable_commands_app.get_visible_commands()
     assert 'has_help_func' in visible_commands
@@ -2281,10 +2214,9 @@ def test_disabled_command_not_in_history(disable_commands_app):
     run_cmd(disable_commands_app, 'has_help_func')
     assert saved_len == len(disable_commands_app.history)
 
-def test_disabled_message_command_name(disable_commands_app, capsys):
+def test_disabled_message_command_name(disable_commands_app):
     message_to_print = '{} is currently disabled'.format(cmd2.cmd2.COMMAND_NAME)
     disable_commands_app.disable_command('has_help_func', message_to_print)
 
-    run_cmd(disable_commands_app, 'has_help_func')
-    out, err = capsys.readouterr()
-    assert err.startswith('has_help_func is currently disabled')
+    out, err = run_cmd(disable_commands_app, 'has_help_func')
+    assert err[0].startswith('has_help_func is currently disabled')
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 47a7a9d6..0916e500 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -122,7 +122,7 @@ def test_complete_bogus_command(cmd2_app):
 
 def test_complete_macro(base_app, request):
     # Create the macro
-    out = run_cmd(base_app, 'macro create fake pyscript {1}')
+    out, err = run_cmd(base_app, 'macro create fake pyscript {1}')
     assert out == normalize("Macro 'fake' created")
 
     # Macros do path completion
diff --git a/tests/test_history.py b/tests/test_history.py
index 9046e877..105dead9 100644
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -16,13 +16,11 @@ except ImportError:
     from unittest import mock
 
 import cmd2
-from cmd2 import clipboard
-from cmd2 import utils
 from .conftest import run_cmd, normalize, HELP_HISTORY
 
 
 def test_base_help_history(base_app):
-    out = run_cmd(base_app, 'help history')
+    out, err = run_cmd(base_app, 'help history')
     assert out == normalize(HELP_HISTORY)
 
 def test_exclude_from_history(base_app, monkeypatch):
@@ -40,14 +38,14 @@ def test_exclude_from_history(base_app, monkeypatch):
     run_cmd(base_app, 'history')
 
     # Verify that the history is empty
-    out = run_cmd(base_app, 'history')
+    out, err = run_cmd(base_app, 'history')
     assert out == []
 
     # Now run a command which isn't excluded from the history
     run_cmd(base_app, 'help')
 
     # And verify we have a history now ...
-    out = run_cmd(base_app, 'history')
+    out, err = run_cmd(base_app, 'history')
     expected = normalize("""    1  help""")
     assert out == expected
 
@@ -126,20 +124,20 @@ def test_history_regex_search(hist):
 def test_base_history(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
-    out = run_cmd(base_app, 'history')
+    out, err = run_cmd(base_app, 'history')
     expected = normalize("""
     1  help
     2  shortcuts
 """)
     assert out == expected
 
-    out = run_cmd(base_app, 'history he')
+    out, err = run_cmd(base_app, 'history he')
     expected = normalize("""
     1  help
 """)
     assert out == expected
 
-    out = run_cmd(base_app, 'history sh')
+    out, err = run_cmd(base_app, 'history sh')
     expected = normalize("""
     2  shortcuts
 """)
@@ -148,7 +146,7 @@ def test_base_history(base_app):
 def test_history_script_format(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
-    out = run_cmd(base_app, 'history -s')
+    out, err = run_cmd(base_app, 'history -s')
     expected = normalize("""
 help
 shortcuts
@@ -159,7 +157,7 @@ def test_history_with_string_argument(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
     run_cmd(base_app, 'help history')
-    out = run_cmd(base_app, 'history help')
+    out, err = run_cmd(base_app, 'history help')
     expected = normalize("""
     1  help
     3  help history
@@ -171,7 +169,7 @@ def test_history_expanded_with_string_argument(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'help history')
     run_cmd(base_app, 'sc')
-    out = run_cmd(base_app, 'history -v shortcuts')
+    out, err = run_cmd(base_app, 'history -v shortcuts')
     expected = normalize("""
     1  alias create sc shortcuts
     4  sc
@@ -184,7 +182,7 @@ def test_history_expanded_with_regex_argument(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'help history')
     run_cmd(base_app, 'sc')
-    out = run_cmd(base_app, 'history -v /sh.*cuts/')
+    out, err = run_cmd(base_app, 'history -v /sh.*cuts/')
     expected = normalize("""
     1  alias create sc shortcuts
     4  sc
@@ -195,7 +193,7 @@ def test_history_expanded_with_regex_argument(base_app):
 def test_history_with_integer_argument(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
-    out = run_cmd(base_app, 'history 1')
+    out, err = run_cmd(base_app, 'history 1')
     expected = normalize("""
     1  help
 """)
@@ -206,7 +204,7 @@ def test_history_with_integer_span(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
     run_cmd(base_app, 'help history')
-    out = run_cmd(base_app, 'history 1..2')
+    out, err = run_cmd(base_app, 'history 1..2')
     expected = normalize("""
     1  help
     2  shortcuts
@@ -217,7 +215,7 @@ def test_history_with_span_start(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
     run_cmd(base_app, 'help history')
-    out = run_cmd(base_app, 'history 2:')
+    out, err = run_cmd(base_app, 'history 2:')
     expected = normalize("""
     2  shortcuts
     3  help history
@@ -228,7 +226,7 @@ def test_history_with_span_end(base_app):
     run_cmd(base_app, 'help')
     run_cmd(base_app, 'shortcuts')
     run_cmd(base_app, 'help history')
-    out = run_cmd(base_app, 'history :2')
+    out, err = run_cmd(base_app, 'history :2')
     expected = normalize("""
     1  help
     2  shortcuts
@@ -240,8 +238,7 @@ def test_history_with_span_index_error(base_app):
     run_cmd(base_app, 'help history')
     run_cmd(base_app, '!ls -hal :')
     with pytest.raises(ValueError):
-        _, err = run_cmd(base_app, 'history "hal :"')
-        assert "ValueError" in err
+        base_app.onecmd('history "hal :"')
 
 def test_history_output_file(base_app):
     run_cmd(base_app, 'help')
@@ -275,7 +272,7 @@ def test_history_edit(base_app, monkeypatch):
 def test_history_run_all_commands(base_app):
     # make sure we refuse to run all commands as a default
     run_cmd(base_app, 'shortcuts')
-    out = run_cmd(base_app, 'history -r')
+    out, err = run_cmd(base_app, 'history -r')
     # this should generate an error, but we don't currently have a way to
     # capture stderr in these tests. So we assume that if we got nothing on
     # standard out, that the error occurred because if the command executed
@@ -283,9 +280,9 @@ def test_history_run_all_commands(base_app):
     assert out == []
 
 def test_history_run_one_command(base_app):
-    expected = run_cmd(base_app, 'help')
-    output = run_cmd(base_app, 'history -r 1')
-    assert output == expected
+    out1, err1 = run_cmd(base_app, 'help')
+    out2, err2 = run_cmd(base_app, 'history -r 1')
+    assert out1 == out2
 
 def test_history_clear(base_app):
     # Add commands to history
@@ -293,72 +290,72 @@ def test_history_clear(base_app):
     run_cmd(base_app, 'alias')
 
     # Make sure history has items
-    out = run_cmd(base_app, 'history')
+    out, err = run_cmd(base_app, 'history')
     assert out
 
     # Clear the history
     run_cmd(base_app, 'history --clear')
 
     # Make sure history is empty
-    out = run_cmd(base_app, 'history')
+    out, err = run_cmd(base_app, 'history')
     assert out == []
 
 def test_history_verbose_with_other_options(base_app):
     # make sure -v shows a usage error if any other options are present
     options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x']
     for opt in options_to_test:
-        output = run_cmd(base_app, 'history -v ' + opt)
-        assert len(output) == 3
-        assert output[1].startswith('Usage:')
+        out, err = run_cmd(base_app, 'history -v ' + opt)
+        assert len(out) == 3
+        assert out[1].startswith('Usage:')
 
 def test_history_verbose(base_app):
     # validate function of -v option
     run_cmd(base_app, 'alias create s shortcuts')
     run_cmd(base_app, 's')
-    output = run_cmd(base_app, 'history -v')
-    assert len(output) == 3
+    out, err = run_cmd(base_app, 'history -v')
+    assert len(out) == 3
     # TODO test for basic formatting once we figure it out
 
 def test_history_script_with_invalid_options(base_app):
     # make sure -s shows a usage error if -c, -r, -e, -o, or -t are present
     options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
     for opt in options_to_test:
-        output = run_cmd(base_app, 'history -s ' + opt)
-        assert len(output) == 3
-        assert output[1].startswith('Usage:')
+        out, err = run_cmd(base_app, 'history -s ' + opt)
+        assert len(out) == 3
+        assert out[1].startswith('Usage:')
 
 def test_history_script(base_app):
     cmds = ['alias create s shortcuts', 's']
     for cmd in cmds:
         run_cmd(base_app, cmd)
-    output = run_cmd(base_app, 'history -s')
-    assert output == cmds
+    out, err = run_cmd(base_app, 'history -s')
+    assert out == cmds
 
 def test_history_expanded_with_invalid_options(base_app):
     # make sure -x shows a usage error if -c, -r, -e, -o, or -t are present
     options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
     for opt in options_to_test:
-        output = run_cmd(base_app, 'history -x ' + opt)
-        assert len(output) == 3
-        assert output[1].startswith('Usage:')
+        out, err = run_cmd(base_app, 'history -x ' + opt)
+        assert len(out) == 3
+        assert out[1].startswith('Usage:')
 
 def test_history_expanded(base_app):
     # validate function of -x option
     cmds = ['alias create s shortcuts', 's']
     for cmd in cmds:
         run_cmd(base_app, cmd)
-    output = run_cmd(base_app, 'history -x')
+    out, err = run_cmd(base_app, 'history -x')
     expected = ['    1  alias create s shortcuts', '    2  shortcuts']
-    assert output == expected
+    assert out == expected
 
 def test_history_script_expanded(base_app):
     # validate function of -s -x options together
     cmds = ['alias create s shortcuts', 's']
     for cmd in cmds:
         run_cmd(base_app, cmd)
-    output = run_cmd(base_app, 'history -sx')
+    out, err = run_cmd(base_app, 'history -sx')
     expected = ['alias create s shortcuts', 'shortcuts']
-    assert output == expected
+    assert out == expected
 
 
 #####
diff --git a/tests/test_pyscript.py b/tests/test_pyscript.py
index 78500185..6981980b 100644
--- a/tests/test_pyscript.py
+++ b/tests/test_pyscript.py
@@ -11,21 +11,15 @@ from .conftest import run_cmd
 def test_pyscript_help(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     python_script = os.path.join(test_dir, 'pyscript', 'help.py')
-    expected = run_cmd(base_app, 'help')
+    out1, err1 = run_cmd(base_app, 'help')
+    out2, err2 = run_cmd(base_app, 'pyscript {}'.format(python_script))
+    assert out1 and out1 == out2
 
-    assert len(expected) > 0
-    assert len(expected[0]) > 0
-    out = run_cmd(base_app, 'pyscript {}'.format(python_script))
-    assert len(out) > 0
-    assert out == expected
 
-
-def test_pyscript_dir(base_app, capsys, request):
+def test_pyscript_dir(base_app, request):
     test_dir = os.path.dirname(request.module.__file__)
     python_script = os.path.join(test_dir, 'pyscript', 'pyscript_dir.py')
 
-    run_cmd(base_app, 'pyscript {}'.format(python_script))
-    out, _ = capsys.readouterr()
-    out = out.strip()
-    assert len(out) > 0
-    assert out == "['cmd_echo']"
+    out, err = run_cmd(base_app, 'pyscript {}'.format(python_script))
+    assert out
+    assert out[0] == "['cmd_echo']"
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 307f69da..4574e579 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -6,6 +6,7 @@ Unit testing for cmd2/utils.py module.
 Copyright 2018 Todd Leonhardt <todd.leonhardt@gmail.com>
 Released under MIT license, see LICENSE file
 """
+import signal
 import sys
 
 import pytest
@@ -216,3 +217,56 @@ def test_stdsim_pause_storage(stdout_sim):
     stdout_sim.pause_storage = True
     stdout_sim.buffer.write(b_str)
     assert stdout_sim.getbytes() == b''
+
+
+@pytest.fixture
+def pr_none():
+    import subprocess
+    command = 'ls'
+    if sys.platform.startswith('win'):
+        command = 'dir'
+    proc = subprocess.Popen([command], shell=True)
+    pr = cu.ProcReader(proc, None, None)
+    return pr
+
+@pytest.mark.skipif(sys.platform == 'linux', reason="Test doesn't work correctly on TravisCI")
+def test_proc_reader_send_sigint(pr_none):
+    assert pr_none._proc.poll() is None
+    pr_none.send_sigint()
+    pr_none.wait()
+    ret_code = pr_none._proc.poll()
+    if sys.platform.startswith('win'):
+        assert ret_code is not None
+    else:
+        assert ret_code == -signal.SIGINT
+
+@pytest.mark.skipif(sys.platform == 'linux', reason="Test doesn't work correctly on TravisCI")
+def test_proc_reader_terminate(pr_none):
+    assert pr_none._proc.poll() is None
+    pr_none.terminate()
+    pr_none.wait()
+    ret_code = pr_none._proc.poll()
+    if sys.platform.startswith('win'):
+        assert ret_code is not None
+    else:
+        assert ret_code == -signal.SIGTERM
+
+@pytest.mark.skipif(sys.platform == 'linux', reason="Test doesn't work correctly on TravisCI")
+def test_proc_reader_wait(pr_none):
+    assert pr_none._proc.poll() is None
+    pr_none.wait()
+    assert pr_none._proc.poll() == 0
+
+
+@pytest.fixture
+def context_flag():
+    return cu.ContextFlag()
+
+def test_context_flag_bool(context_flag):
+    assert not context_flag
+    with context_flag:
+        assert context_flag
+
+def test_context_flag_exit_err(context_flag):
+    with pytest.raises(ValueError):
+        context_flag.__exit__()