def callprint(self, cmd, shell=None, lock_file=None, nofail=False, container=None):
        """
        Prints the command, and then executes it, then prints the memory use and
        return code of the command.

        Uses python's subprocess.Popen() to execute the given command. The shell argument is simply
        passed along to Popen(). You should use shell=False (default) where possible, because this enables memory
        profiling. You should use shell=True if you require shell functions like redirects (>) or stars (*), but this
        will prevent the script from monitoring memory use. The pipes (|) will be used to split the command into
        subprocesses run within python, so the memory profiling is possible.

        cmd can also be a series (a dict object) of multiple commands, which will be run in succession.

        :param str | Iterable[str] cmd: Bash command(s) to be run.
        :param str lock_file: a lock file name
        :param bool nofail: Should the pipeline bail on a nonzero return from a process? Default: False
            Nofail can be used to implement non-essential parts of the pipeline; if these processes fail,
            they will not cause the pipeline to bail out.
        :param bool shell: if the command should be run it its own shell, default: None (will try
            to determine based on the command)
        :param container: Named Docker container in which to execute.
        :param container: str
        """
        # The Popen shell argument works like this:
        # if shell=False, then we format the command (with split()) to be a list of command and its arguments.
        # Split the command to use shell=False;
        # leave it together to use shell=True;

        def get_mem_child_sum(proc):
            try:
                # get children processes
                children = proc.children(recursive=True)
                # get RSS memory of each child proc and sum all
                mem_sum = sum([x.memory_info().rss for x in children])
                # return in gigs
                return mem_sum/1e9
            except (psutil.NoSuchProcess, psutil.ZombieProcess) as e:
                print(e)
                print("Warning: couldn't add memory use for process: {}".format(proc.pid))
                return 0


        def display_memory(memval):
            return None if memval < 0 else "{}GB".format(round(memval, 3))

        def make_dict(command):
            a, s = (command, True) if check_shell(command, shell) else (shlex.split(command), False)
            return dict(args=a, stdout=subprocess.PIPE, shell=s)

        if container:
            cmd = "docker exec " + container + " " + cmd

        param_list = [make_dict(c) for c in split_by_pipes(cmd)] \
            if check_shell_pipes(cmd) else [dict(args=cmd, stdout=None, shell=True)]

        proc_name = get_proc_name(cmd)

        # stop_times = []
        processes = []
        running_processes = []
        start_time = time.time()
        for i in range(len(param_list)):
            running_processes.append(i)
            if i == 0:
                processes.append(psutil.Popen(preexec_fn=os.setpgrp, **param_list[i]))
            else:
                param_list[i]["stdin"] = processes[i - 1].stdout
                processes.append(psutil.Popen(preexec_fn=os.setpgrp, **param_list[i]))

            self.procs[processes[-1].pid] = {
                "proc_name": proc_name,
                "subproc_name" : get_proc_name(param_list[i]["args"]),
                "start_time": start_time,
                "container": container,
                "p": processes[-1]
            }

        self._report_command(cmd, [x.pid for x in processes])
            # Capture the subprocess output in <pre> tags to make it format nicely
            # if the markdown log file is displayed as HTML.
        print("<pre>")

        local_maxmems = [-1] * len(running_processes)
        returncodes = [None] * len(running_processes)

        if not self.wait:
            print("</pre>")
            ids = [x.pid for x in processes]
            print ("Not waiting for subprocess(es): " + str(ids))
            return [0, -1]

        def proc_wrapup(i):
            """
            :param i: internal ID number of the subprocess
            """
            returncode = processes[i].returncode
            current_pid = processes[i].pid

            info = "Process {pid} returned {ret}; memory: {mem}. ".format(
                pid=current_pid, 
                ret=processes[i].returncode,
                mem=display_memory(local_maxmems[i]))
            
            # report process profile
            self._report_profile(self.procs[current_pid]["proc_name"], lock_file, time.time() - self.procs[current_pid]["start_time"], local_maxmems[i])

            # Remove this as a running subprocess
            del self.procs[current_pid]
            running_processes.remove(i)

            returncodes[i] = returncode
            return info


        sleeptime = .0001
        info = ""
        while running_processes:
            for i in running_processes:
                local_maxmems[i] = max(local_maxmems[i], (get_mem_child_sum(processes[i])))
                self.peak_memory = max(self.peak_memory, local_maxmems[i])
                if not self._attend_process(processes[i], sleeptime):
                    info += proc_wrapup(i)

            # the sleeptime is extremely short at the beginning and gets longer exponentially 
            # (+ constant to prevent copious checks at the very beginning)
            # = more precise mem tracing for short processes
            sleeptime = min((sleeptime + 0.25) * 3 , 60/len(processes))

        # All jobs are done, print a final closing and job info
        stop_time = time.time()
        info += " Elapsed: " + str(datetime.timedelta(seconds=self.time_elapsed(start_time))) + "."
        info += " Peak memory: {pipe}.".format(pipe=display_memory(self.peak_memory))

        print("</pre>")
        print(info)

        for rc in returncodes:
            if rc != 0:
                msg = "Subprocess returned nonzero result. Check above output for details"
                self._triage_error(SubprocessError(msg), nofail)



        return [returncodes, local_maxmems]
Prints the command, and then executes it, then prints the memory use and
        return code of the command.

        Uses python's subprocess.Popen() to execute the given command. The shell argument is simply
        passed along to Popen(). You should use shell=False (default) where possible, because this enables memory
        profiling. You should use shell=True if you require shell functions like redirects (>) or stars (*), but this
        will prevent the script from monitoring memory use. The pipes (|) will be used to split the command into
        subprocesses run within python, so the memory profiling is possible.

        cmd can also be a series (a dict object) of multiple commands, which will be run in succession.

        :param str | Iterable[str] cmd: Bash command(s) to be run.
        :param str lock_file: a lock file name
        :param bool nofail: Should the pipeline bail on a nonzero return from a process? Default: False
            Nofail can be used to implement non-essential parts of the pipeline; if these processes fail,
            they will not cause the pipeline to bail out.
        :param bool shell: if the command should be run it its own shell, default: None (will try
            to determine based on the command)
        :param container: Named Docker container in which to execute.
        :param container: str