如何从一个subprocess.Popen命令同步运行多个命令?

时间:2016-09-27 10:10:09

标签: python subprocess python-3.5 python-2.6

是否可以使用相同的子进程命令按顺序执行任意数量的命令?

我需要每个命令在执行之前等待前一个命令完成,我需要它们全部在同一个会话/ shell中执行。我还需要它在Python 2.6,Python 3.5中工作。我还需要subprocess命令才能在Linux,Windows和macOS中工作(这就是为什么我只是在这里使用echo命令)。

请参阅以下非工作代码:

import sys
import subprocess

cmds = ['echo start', 'echo mid', 'echo end']

p = subprocess.Popen(cmd=tuple([item for item in cmds]),
                     stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    sys.stdout.flush()
    print(">>> " + line.rstrip())

如果无法做到这一点,我应采取哪种方法在同一会话/ shell中以同步顺序执行命令?

6 个答案:

答案 0 :(得分:5)

如果要在同一个会话/ shell 中一​​个接一个地执行许多命令,则必须启动一个shell并使用所有命令提供它,一次一个,然后是新行,并在最后关闭管道。如果某些命令不是真正的进程,而是shell命令可能会改变shell环境,这是有意义的。

在Windows下使用Python 2.7的示例:

encoding = 'latin1'
p = subprocess.Popen('cmd.exe', stdin=subprocess.PIPE,
             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for cmd in cmds:
    p.stdin.write(cmd + "\n")
p.stdin.close()
print p.stdout.read()

要在Linux下运行此代码,您必须将cmd.exe替换为/bin/bash,并可能将编码更改为utf8。

对于Python 3,您必须对命令进行编码并可能对其输出进行解码,并将括号与print一起使用。

注意:这只能用于少量输出。如果在关闭stdin管道之前有足够的输出来填充管道缓冲区,则此代码将会死锁。更强大的方法是让第二个线程读取命令的输出以避免该问题。

答案 1 :(得分:3)

一种可能的解决方案,看起来像是在同一个shell中运行:

subprocess.Popen('echo start;echo mid;echo end', shell=True)

注意 - 如果将命令作为字符串传递,则shell必须为True 注意 - 这只适用于linux,您可能需要在Windows上找到类似的方法。

希望它会有所帮助。

来自python doc -

  

在Unix上,shell = True,shell默认为/ bin / sh。如果args是   string,string指定通过shell执行的命令。   这意味着字符串必须完全按照原样进行格式化   在shell提示符下键入时。

答案 2 :(得分:2)

这与Serge Ballesta发布的答案类似,但并不完全如此。使用他的异步执行,你不关心结果。使用我的同步处理和结果收集。就像他的回答一样,我在这里展示Windows解决方案 - 在Linux中运行bash进程而不是在Windows中运行cmd。

from subprocess import Popen, PIPE
process = Popen( "cmd.exe", shell=False, universal_newlines=True,
                  stdin=PIPE, stdout=PIPE, stderr=PIPE )                             
out, err = process.communicate( commands ) 

使用详细信息:此处传递给commands方法的process.communicate参数是换行符分隔字符串。例如,如果您只是将批处理文件内容读入字符串,则可以通过这种方式运行它,因为它已经有了换行符。 重要:您的字符串必须以换行符"\n"结尾。如果没有,则最终命令将无法执行。就像你在命令提示符下键入它但在最后没有点击enter一样。但是,您会在返回的stdout末尾看到一条神秘的More?行。 (如果遇到这个原因,那就是原因。)

process.communicate按定义同步运行,并返回stdout和stderr消息(如果你将它们指向Popen构造函数中的subprocess.PIPE)。

当您以这种方式创建cmd.exe进程并向其传递字符串时,结果将与您打开命令提示符窗口并输入命令完全相同。我的意思是字面意思。如果你测试它,你会看到返回的stdout包含你的命令。 (如果在执行批处理文件时包含@echo off,则无关紧要。)

关心那些关心“清洁”标准结果的人的提示:

  • @echo off不会禁止您的命令出现在此返回的字符串中,但它会删除额外的新行,否则会在那里找到它们的方式。 (universal_newlines = True删除另一组)

  • 在命令中包含@符号前缀允许它们仍然执行。在“正常”批处理过程中,这是“隐藏”命令的逐行方式。在这种情况下,它是一个安全的标记,您可以通过它找到要删除的标准行。 (如果有人如此倾向)

  • cmd.exe“标题”将出现在您的输出中(表示Windows的版本等)。由于您可能希望使用@echo off启动命令集,以删除额外的换行符,这也是查找标题行停止位置以及命令/结果开始的好方法。

最后,为了解决关于“大”输出填充管道并导致问题的担忧 - 首先我认为你需要回收大量数据才能成为一个问题 - 比大多数人在用例中遇到的更多。其次,如果它确实是一个问题,只需打开一个文件进行编写,并将该文件句柄(对文件对象的引用)传递给stdout / err而不是PIPE。然后,使用您创建的文件执行任何操作。

答案 3 :(得分:1)

这个在python 2.7中工作,也应该在windows中工作。 python> 3可能需要一些细微的改进。

产生的输出是(使用日期和睡眠很容易看到命令在行中执行):

>>>Die Sep 27 12:47:52 CEST 2016
>>>
>>>Die Sep 27 12:47:54 CEST 2016

如您所见,命令连续执行。

    import sys
    import subprocess
    import shlex

    cmds = ['date', 'sleep 2', 'date']

    cmds = [shlex.split(x) for x in cmds]

    outputs =[]
    for cmd in cmds:
        outputs.append(subprocess.Popen(cmd,
                                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate())


    for line in outputs:
        print ">>>" + line[0].strip()

这是我与@Marichyasana合并得到的答案:

import sys
import os


def run_win_cmds(cmds):

    @Marichyasana code (+/-)

def run_unix_cmds(cmds):

    import subprocess
    import shlex


    cmds = [shlex.split(x) for x in cmds]

    outputs =[]
    for cmd in cmds:
        outputs.append(subprocess.Popen(cmd,
                                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate())


    rc = ''
    for line in outputs:
        rc +=  line[0].strip()+'\n'

    return rc


cmds = ['date', 'sleep 2', 'date']

if os.name == 'nt':
     run_win_cmds(cmds)
elif os.name == 'posix':
    run_unix_cmds(cmds)

问这个是不适合你的需求! ;)

答案 4 :(得分:1)

这是我使用的功能(以及运行它的主要功能)。我会说你可以用它来解决你的问题。它很灵活。

# processJobsInAList.py
# 2016-09-27   7:00:00 AM   Central Daylight Time 

import win32process, win32event

def CreateMyProcess2(cmd):
    ''' create process width no window that runs a command with arguments
    and returns the process handle'''
    si   = win32process.STARTUPINFO()
    info = win32process.CreateProcess(
        None,      # AppName
        cmd,       # Command line
        None,      # Process Security
        None,      # Thread Security
        0,         # inherit Handles?
        win32process.NORMAL_PRIORITY_CLASS,
        None,      # New environment
        None,      # Current directory
        si)        # startup info
    # info is tuple (hProcess, hThread, processId, threadId)
    return info[0]

if __name__ == '__main__' :
    ''' create/run a process for each list element in "cmds"
    output may be out of order because processes run concurrently '''

    cmds=["echo my","echo heart","echo belongs","echo to","echo daddy"]
    handles    = []
    for i in range(len(cmds)):
        cmd    = 'cmd /c ' + cmds[i]
        handle = CreateMyProcess2(cmd)
        handles.append(handle)

    rc = win32event.WaitForMultipleObjects( handles, 1, -1)  # 1 wait for all, -1 wait infinite
    print 'return code ',rc

输出:
心脏

属于

爸爸
返回码0

更新:如果你想运行相同的过程,它将为你序列化事物:
1)删除行:handles.append(句柄)
2)将“变量”句柄替换为“WaitFor”行上的“句柄”列表 3)替换WaitForSingleObject代替WaitForMultipleObjects

答案 5 :(得分:0)

这是我的看法(无需重新启动 shell 等)

功能:

  • 管道重定向(在此示例中,下面的代码充当日志,但可用于任何其他功能)

  • 子进程的软关闭和硬关闭

  • 后台监听器

在 Windows 操作系统上测试过,但通常也可以在 Linux 上运行。

代码

import glob
import os
import pathlib
import traceback
import logging
from datetime import datetime
from subprocess import Popen, PIPE, STDOUT
from threading import Thread, Event
from typing import Union
from enum import Enum

LOG_FOLDER = "{0}\{1}".format(pathlib.Path().absolute(), "log")
GDB_FOLDER = "C:\\MinGW\\bin"
GDB_EXE = "gdb.exe"
GDB_PY_EXE = "gdb-python27.exe"
CMD_EXE = "C:\\Windows\\system32\\cmd.exe"
CALC_EXE = "C:\\Windows\\system32\\win32calc.exe"


class LOG_TYPE(Enum):
    Info = 0,
    Warning = 1,
    Error = 2,
    Critical = 3


class STD_LOG(object):
    def __init__(self, name: str, log_enabled: bool = True, print_enabled: bool = True,
                 detailed_log: bool = False) -> None:
        self.log_enabled = log_enabled
        self.print_enabled = print_enabled
        self.filename = "{0}\{1}{2}.log".format(LOG_FOLDER, name,
                                                datetime.now().strftime("-%d_%m_%Y"))  # "-%d_%m_%Y-%H_%M_%S"
        self.logger = logging.getLogger('CLI_LOGGER')
        self.logger.setLevel(logging.DEBUG)
        handler = logging.FileHandler(filename=self.filename, mode="a", encoding="utf-8")
        formatter = logging.Formatter('%(message)s')
        if detailed_log:
            formatter = logging.Formatter('%(asctime)s \t %(name)s \t %(levelname)s: \n%(message)s \n')
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)

    def output(self, data: str, logtype: LOG_TYPE = LOG_TYPE.Info):
        if not data:
            return None
        if self.print_enabled:
            print(data)
        if self.log_enabled:
            if logtype == LOG_TYPE.Info:
                self.logger.info(msg=data)
            elif logtype == LOG_TYPE.Warning:
                self.logger.warning(msg=data)
            elif logtype == LOG_TYPE.Error:
                self.logger.error(msg=data)
            else:
                self.logger.critical(msg=data)

        # FOR STACKOVERFLOW Example
        # if api_call activated => run api call from a server
        # if output == "desired result":
        #   Do something
        # Etc.

    def input(self, data: str):
        pass
        # FOR STACKOVERFLOW
        # Perhaps a separate log file?
        # Or redirect to output, etc.
        # self.output(data=data)


# inspiration for killable thread -> https://stackoverflow.com/a/49877671
class CLI_THREAD(Thread):
    def __init__(self, source: str, logger: STD_LOG, sleep_interval: float = 0.25) -> None:
        super().__init__()
        self._close = Event()
        self._interval = sleep_interval
        self.base = Popen(source, stdin=PIPE, stdout=PIPE, stderr=STDOUT, text=True, shell=True, close_fds=True)
        self.logger = logger
        self.logger.output(data="CLI STARTED [Process ID: {0}]".format(self.pid()))

    def cli_alive(self) -> Union[bool, int]:
        if self.base.poll() is None:
            return True
        else:
            return False

    def pid(self) -> int:
        return self.base.pid

    def run(self) -> None:
        while True:
            try:
                if not self.cli_alive():
                    break

                if self.base.stdout.readable():
                    output = self.base.stdout.readline().strip()
                    self.logger.output(data=output)

                closing = self._close.wait(self._interval)
                if closing and self.base.stdout.closed:
                    break
            except Exception as ex:
                ex_msg = ''.join(traceback.format_exception(None, ex, ex.__traceback__))
                self.logger.output(data=ex_msg, logtype=LOG_TYPE.Error)

        self.logger.output(data="End of CLI Thread")

    def close(self) -> None:
        self._close.set()

    def terminate(self) -> None:
        if self.cli_alive():
            self.base.terminate()
            self.logger.output(data="Terminate function activated", logtype=LOG_TYPE.Warning)


class CLI(object):
    def __init__(self, name: str, source: str, close_arg: str = None, echo: bool = True) -> None:
        self.logger = STD_LOG(name)
        self._cli_thread = CLI_THREAD(source=source, logger=self.logger)
        self._close_arg = close_arg
        self._cli_thread.start()  # start listening to console
        if not echo:
            self.execute("@echo off")

    def close(self):
        if self._close_arg:
            self.execute(command=self._close_arg)
        self._cli_thread.close()

    def cleanup(self):
        self._cli_thread.base.terminate()
        del self._cli_thread

    def __exit__(self, exc_type, exc_value, traceback):
        self.cleanup()

    def execute(self, command: str):
        if self._cli_thread.is_alive():
            self._cli_thread.base.stdin.write(command + "\n")
            self._cli_thread.base.stdin.flush()
        else:
            self.logger.output(data="Sending command to CLOSED THREAD", logtype=LOG_TYPE.Error)


def empty_log():
    files = glob.glob("{0}/*".format(LOG_FOLDER))
    for f in files:
        os.remove(f)


def main():
    cli = CLI(name="cli_1", source=CMD_EXE, close_arg="exit")
    try:
        cli.execute(command="cd {0}".format(GDB_FOLDER))
        cli.execute(command=GDB_EXE)
        cli.execute(command="file C:/Windows/system32/win32calc.exe")
        cli.execute(command="quit")
        cli.close()
    except Exception as e:
        msg = ''.join(traceback.format_exception(None, e, e.__traceback__))
        cli.logger.output(data=msg, logtype=LOG_TYPE.Critical)
        cli.cleanup()


if __name__ == '__main__':
    empty_log()
    main()

输出(cli-somedate.log)

<块引用>

CLI 已启动 [进程 ID:9720]
Microsoft Windows [版本 10.0.17763.1728] (c) 2018 Microsoft Corporation。保留所有权利。
C:\Users\Administrator\PycharmProjects\ProgramWatcher\src\GDB>cd C:\MinGW\bin
C:\MinGW\bin>gdb.exe
GNU gdb (GDB) 7.6.1 版权所有 (C) 2013 Free Software Foundation, Inc. 许可证 GPLv3+:GNU GPL 版本 3 或更高版本http://gnu.org/licenses/gpl.html
这是免费软件:您可以自由更改和重新分发它。在法律允许的范围内,不提供任何保证。输入“显示复印”和“显示保修”了解详情。
这个 GDB 被配置为“mingw32”。有关错误报告的说明,请参阅:http://www.gnu.org/software/gdb/bugs/
(gdb) 从 C:\Windows\system32\win32calc.exe 读取符号...(未找到调试符号)...完成。
(gdb) C:\MinGW\bin>退出
CLI 线程结束