简体   繁体   English

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

[英]How to run multiple commands synchronously from one subprocess.Popen command?

Is it possible to execute an arbitrary number of commands in sequence using the same subprocess command?是否可以使用相同的子进程命令按顺序执行任意数量的命令?

I need each command to wait for the previous one to complete before executing and I need them all to be executed in the same session/shell.我需要每个命令在执行之前等待前一个命令完成,并且我需要它们都在同一个会话/shell中执行。 I also need this to work in Python 2.6, Python 3.5.我还需要它在 Python 2.6、Python 3.5 中工作。 I also need the subprocess command to work in Linux, Windows and macOS (which is why I'm just using echo commands as examples here).我还需要 subprocess 命令在 Linux、Windows 和 macOS 中工作(这就是为什么我在这里只使用echo命令作为示例)。

See non-working code below:请参阅下面的非工作代码:

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())

If this is not possible, which approach should I take in order to execute my commands in synchronous sequence within the same session/shell?如果这是不可能的,我应该采取哪种方法来在同一个会话/外壳中以同步顺序执行我的命令?

If you want to execute many commands one after the other in the same session/shell , you must start a shell and feed it with all the commands, one at a time followed by a new line, and close the pipe at the end.如果你想在同一个session/shell 中一个接一个地执行许多命令,你必须启动一个 shell 并提供所有命令,一次一个,然后一个新行,最后关闭管道。 It makes sense if some commands are not true processes but shell commands that could for example change the shell environment.如果某些命令不是真正的进程而是可以例如更改 shell 环境的 shell 命令,那么这是有道理的。

Example using Python 2.7 under Windows:在 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()

To have this code run under Linux, you would have to replace cmd.exe with /bin/bash and probably change the encoding to utf8.要在 Linux 下运行此代码,您必须将cmd.exe替换为/bin/bash并且可能将编码更改为 utf8。

For Python 3, you would have to encode the commands and probably decode their output, and to use parentheses with print.对于 Python 3,您必须对命令进行编码并可能对它们的输出进行解码,并在打印时使用括号。

Beware: this can only work for little output.当心:这只适用于很少的输出。 If there was enough output to fill the pipe buffer before closing the stdin pipe, this code would deadlock.如果在关闭 stdin 管道之前有足够的输出来填充管道缓冲区,则此代码将死锁。 A more robust way would be to have a second thread to read the output of the commands to avoid that problem.更可靠的方法是让第二个线程读取命令的输出以避免该问题。

This is similar to the answer posted by Serge Ballesta, but not quite.这类似于 Serge Ballesta 发布的答案,但不完全相同。 Use his for asynchronous execution, where you don't care about the results.使用他的异步执行,你不关心结果。 Use mine for synchronous processing and result gathering.使用我的进行同步处理和结果收集。 Like his answer, I'm showing the Windows solution here - run a bash process in Linux rather than cmd in Windows.就像他的回答一样,我在这里展示了 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 ) 

USAGE DETAILS: The commands argument being passed here to the process.communicate method is a newline delimited string.用法详细信息:这里传递给process.communicate方法的commands参数是一个换行符分隔的字符串。 If, for example you just read a batch file contents into a string, you could run it this way because it would already have the newlines.例如,如果您只是将批处理文件内容读入字符串,则可以以这种方式运行它,因为它已经有了换行符。

Important : your string must end in a newline "\\n" .重要提示:您的字符串必须以换行符"\\n"结尾。 If it does not, that final command will fail to execute.如果没有,则最终命令将无法执行。 Just like if you typed it into your command prompt but didn't hit enter at the end.就像您在命令提示符中键入它但最后没有按enter You will however see a mysterious More?然而,你会看到一个神秘的More? line in the end of the stdout returned.返回的标准输出末尾的行。 (that's the cause if you encounter this). (如果您遇到这种情况,这就是原因)。

process.communicate runs synchronously by definition, and returns the stdout and stderr messages (if you directed them to subprocess.PIPE in your Popen constructor). process.communicate运行同步的定义,并返回的输出和错误消息(如果你指示他们subprocess.PIPE在POPEN构造函数)。

When you create a cmd.exe process in this way, and pass a string to it, the results will be exactly like if you were to open a command prompt window and entered commands into.当您以这种方式创建一个cmd.exe进程,并将一个字符串传递给它时,结果将与您打开命令提示符窗口并在其中输入命令完全一样。 And I mean that quite literally.我的意思是字面意思。 If you test this, you will see that the stdout which is returned contains your commands.如果您对此进行测试,您将看到返回的标准输出包含您的命令。 (It does NOT matter if you include an @echo off like if executing a batch file). (如果您像执行批处理文件一样包含@echo off并不重要)。

Tips for those who care about "clean" stdout results:给那些关心“干净”标准输出结果的人的提示:

  • @echo off will not suppress your commands from appearing in this returned string, but it does remove extra newlines that find their way in there otherwise. @echo off不会禁止你的命令出现在这个返回的字符串中,但它会删除额外的换行符,否则它们会在那里找到。 (universal_newlines=True strips an another set of those) (universal_newlines=True 剥离另一组那些)

  • Including an @ symbol prefix to your commands allows them to still execute.在命令中包含@符号前缀允许它们仍然执行。 In a "normal" batch process that's the line-by-line way to "hide" your commands.在“正常”批处理中,这是“隐藏”命令的逐行方式。 In this context, it's a safe an easy marker by which you can find stdout lines you want to remove.在这种情况下,它是一个安全的简单标记,您可以通过它找到要删除的标准输出行。 (if one were so inclined) (如果有人这么倾向的话)

  • The cmd.exe "header" will appear in your output (which says the version of Windows etc.). cmd.exe“标题”将出现在您的输出中(表示 Windows 的版本等)。 Since you probably want to start your set of commands with @echo off , to cut out the extra newlines, that is also a great way to find where the header lines stopped and your commands/results began.由于您可能希望使用@echo off开始您的命令集,以删除额外的换行符,这也是查找标题行停止位置和命令/结果开始位置的好方法。

Finally, to address concerns about "large" output filling the pipes and causing you problems - first I think you need a HUGE amount of data coming back for that to be an issue - more than most people will encounter in their use cases.最后,为了解决对“大”输出填充管道并导致问题的担忧 - 首先,我认为您需要返回大量数据才能成为问题 - 比大多数人在他们的用例中遇到的要多。 Second, if it really is a concern just open a file for writing and pass that file handle (the reference to the file object) to stdout/err instead of PIPE .其次,如果真的是一个问题,只需打开一个文件进行写入并将该文件句柄(对文件对象的引用)传递给 stdout/err 而不是PIPE Then, do whatever you want with the file you've created.然后,对您创建的文件执行任何您想要的操作。

One possible solution, looks like its running in same shell:一种可能的解决方案,看起来像是在同一个 shell 中运行:

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

Note - If you pass your command as a string then shell has to be True Note - This is working on linux only, you may have to find something similar way out on windows.注意 - 如果您将命令作为字符串传递,则 shell 必须为 True 注意 - 这仅适用于 linux,您可能需要在 Windows 上找到类似的方法。

Hope it will help.希望它会有所帮助。

From python doc -来自 python 文档 -

On Unix with shell=True, the shell defaults to /bin/sh.在 shell=True 的 Unix 上,shell 默认为 /bin/sh。 If args is a string, the string specifies the command to execute through the shell.如果 args 是字符串,则该字符串指定要通过 shell 执行的命令。 This means that the string must be formatted exactly as it would be when typed at the shell prompt.这意味着字符串的格式必须与在 shell 提示符下键入时完全相同。

Here is a function (and main to run it) that I use.这是我使用的一个函数(和运行它的主要函数)。 I would say that you can use it for your problem.我会说你可以用它来解决你的问题。 And it is flexible.而且它很灵活。

# 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

output:输出:
heart
my我的
belongs属于
to
daddy爸爸
return code 0返回码 0

UPDATE: If you want to run the same process, which will serialize things for you:更新:如果你想运行相同的进程,它会为你序列化:
1) Remove line: handles.append(handle) 1) 删除行:handles.append(handle)
2) Substitute the variable "handle" in place of the list "handles" on the "WaitFor" line 2) 用变量“handle”代替“WaitFor”行上的“handles”列表
3) Substitute WaitForSingleObject in place of WaitForMultipleObjects 3) 用 WaitForSingleObject 代替 WaitForMultipleObjects

This one works in python 2.7 and should work also in windows.这个适用于python 2.7,也应该适用于Windows。 Probably some small refinement is needed for python >3. python > 3可能需要一些小的改进。

The produced output is (using date and sleep it is easy to see that the commands are executed in row):产生的输出是(使用 date 和 sleep 很容易看出命令是按行执行的):

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

As you see the commands are executed in a row.如您所见,命令是连续执行的。

    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()

This is what I obtain merging with @Marichyasana answer:这是我与@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)

Ask is this one do not fit your needs!请问这个不适合您的需求! ;) ;)

Here is my take on it (Without having to restart a shell and whatnot)这是我的看法(无需重新启动 shell 之类的)

Functions:职能:

  • Pipeline redirect (in this example code below acting as a log, but can be used for any other functionality)管道重定向(在此示例代码中作为日志,但可用于任何其他功能)

  • Soft and Hard close of the subprocess子进程的软关闭和硬关闭

  • Background listener后台监听器

Tested on Windows OS, but should normally also work on Linux.在 Windows 操作系统上测试过,但通常也可以在 Linux 上运行。

Code代码

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()

Output (cli-somedate.log)输出(cli-somedate.log)

CLI STARTED [Process ID: 9720] CLI 已启动 [进程 ID:9720]
Microsoft Windows [Version 10.0.17763.1728] (c) 2018 Microsoft Corporation. Microsoft Windows [版本 10.0.17763.1728] (c) 2018 Microsoft Corporation。 All rights reserved.版权所有。
C:\\Users\\Administrator\\PycharmProjects\\ProgramWatcher\\src\\GDB>cd C:\\MinGW\\bin C:\\Users\\Administrator\\PycharmProjects\\ProgramWatcher\\src\\GDB>cd C:\\MinGW\\bin
C:\\MinGW\\bin>gdb.exe C:\\MinGW\\bin>gdb.exe
GNU gdb (GDB) 7.6.1 Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html GNU gdb (GDB) 7.6.1 版权所有 (C) 2013 Free Software Foundation, Inc. 许可证 GPLv3+:GNU GPL 版本 3 或更高版本http://gnu.org/licenses/gpl.html
This is free software: you are free to change and redistribute it.这是免费软件:您可以自由更改和重新分发它。 There is NO WARRANTY, to the extent permitted by law.在法律允许的范围内,不提供任何保证。 Type "show copying" and "show warranty" for details.输入“显示复制”和“显示保修”了解详细信息。
This GDB was configured as "mingw32".这个 GDB 被配置为“mingw32”。 For bug reporting instructions, please see: http://www.gnu.org/software/gdb/bugs/ .有关错误报告的说明,请参阅: http : //www.gnu.org/software/gdb/bugs/
(gdb) Reading symbols from C:\\Windows\\system32\\win32calc.exe...(no debugging symbols found)...done. (gdb) 从 C:\\Windows\\system32\\win32calc.exe 读取符号...(未找到调试符号)...完成。
(gdb) C:\\MinGW\\bin>exit (gdb) C:\\MinGW\\bin>退出
End of CLI Thread CLI 线程结束

Here is an example of how to do it synchronously if you want to run the commands one after the other, and react / see the output of each before the previous is completed...这是一个如何同步执行的示例,如果您想一个接一个地运行命令,并在前一个完成之前做出反应/查看每个的 output...

As opposed to all other solutions, this one allows you to react/change the flow of your commands as you can parse the results after each command...与所有其他解决方案相反,此解决方案允许您响应/更改命令流,因为您可以在每个命令之后解析结果......

import os
import time
import subprocess

p =  subprocess.Popen('bash', stdin=subprocess.PIPE,
         stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8')
os.set_blocking(p.stdout.fileno(), False)
os.set_blocking(p.stderr.fileno(), False)
#some commands...
cmds = [
    'echo foo',
    'ls',
    'echo bar'
]
i=0
out = ['']
for cmd in cmds:
    p.stdin.write(cmd + "\n")
    p.stdin.write("echo ================\n")
    more_out = []
    while not more_out or more_out[-1]!="================\n":
        p.stdin.flush()
        p.stdout.flush()
        p.stdout.flush()
        time.sleep(.01)
        more_out = p.stdout.readlines()
        err = p.stdout.readlines()
        if more_out:
            out.extend(more_out)
            print(''.join(more_out))
        if more_err:
            print('ERROR:', err)
            break

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM