Run this program with Parameters, Redirects, and Pipes. If shell=True, this command is
        executed as a string directly on the shell; otherwise, it's executed using Popen processes
        and appropriate streams.
        :param args: 0 or more of Parameter, Redirect, and Pipe
        :param kwargs: shell=Bool
        :return: None

def run(self, *args, **kwargs):
        """
        Run this program with Parameters, Redirects, and Pipes. If shell=True, this command is
        executed as a string directly on the shell; otherwise, it's executed using Popen processes
        and appropriate streams.
        :param args: 0 or more of Parameter, Redirect, and Pipe
        :param kwargs: shell=Bool
        :return: None
        """
        # Get default for kwarg shell
        shell = kwargs.get('shell', False)

        # Output log info for this command
        run_cmd = self.__generate_cmd(*args, shell=True)
        log_header = 'Running {}\n{}\n'.format(self.software_name, run_cmd)
        if _Settings.logger._is_active():
            _Settings.logger._write(log_header)
        else:
            sys.stdout.write(log_header)

        # If shell is True, execute this command directly as a string
        if shell:
            subprocess.call(run_cmd, shell=True, executable=os.environ['SHELL'])  #TODO Check to see if this works on Windows
        else:
            # Get the command blueprint for this call
            cmd_blueprint = self.__generate_cmd(*args, shell=False)
            output_stream_filehandles = []
            blueprint_processes = []

            # For each command in the blueprint, set up streams and Popen object to execute
            for i, cmd in enumerate(cmd_blueprint):
                stdin_stream = None if i == 0 else blueprint_processes[i - 1].stdout
                stdout_filehandle = None
                stderr_filehandle = None

                # If this command isn't the last in the list, that means the output
                # is being piped into the next command
                if i + 1 < len(cmd_blueprint):
                    stdout_filehandle = subprocess.PIPE
                # If this is the last command in the list, stdout may be redirected to a file...
                elif cmd['stdout']:
                    redir = cmd['stdout']
                    stdout_filehandle = open(redir.dest, redir.mode)
                    output_stream_filehandles.append(stdout_filehandle)
                # ...or it may be set out to the main log file
                elif _Settings.logger.log_stdout and _Settings.logger.destination:
                    stdout_filehandle = subprocess.PIPE

                # stderr can be redirected regardless of piping
                if cmd['stderr']:
                    redir = cmd['stderr']
                    stderr_filehandle = open(redir.dest, redir.mode)
                    output_stream_filehandles.append(stderr_filehandle)
                # Or it may be sent out to a log file
                elif (
                        _Settings.logger.log_stderr
                        and (_Settings.logger.destination_stderr or _Settings.logger.destination)
                ):
                    stderr_filehandle = subprocess.PIPE

                # Create this process as a Popen object, with appropriate streams
                process = subprocess.Popen(cmd['cmd'], stdin=stdin_stream,
                                 stdout=stdout_filehandle, stderr=stderr_filehandle)
                blueprint_processes.append(process)

                # If this is the last command in the list, wait for it to finish
                if i + 1 == len(cmd_blueprint):
                    process.wait()

                    # If logging is set, capture stdout (or stderr) to log file
                    # TODO I think the logic here can be expressed more concisely
                    if _Settings.logger.log_stdout and _Settings.logger.destination:
                        for line in process.stdout:
                            _Settings.logger._write(line)
                    if (
                            _Settings.logger.log_stderr
                            and (_Settings.logger.destination_stderr or _Settings.logger.destination)
                    ):
                        for line in process.stderr:
                            _Settings.logger._write(line, bool(_Settings.logger.destination_stderr))

            # Close all the file handles created for redirects
            map(lambda f: f.close(), output_stream_filehandles)