如何写入 Python 子进程的 stdin?

我正在尝试编写一个 Python 脚本,它启动一个子进程,并写入子进程 stdin。我还希望能够确定子进程崩溃时要采取的操作。

我试图启动的过程是一个名为 nuke的程序,它有自己的内置 Python 版本,我希望能够向其提交命令,然后告诉它在命令执行后退出。到目前为止,我已经算出,如果我在命令提示符上启动 Python,然后启动 nuke作为一个子进程,然后我可以输入命令到 nuke,但我希望能够把这一切放在一个脚本,这样主 Python 程序可以启动 nuke,然后写到它的 标准输入(因此写入它的内置版本的 Python) ,并告诉它做时髦的事情,所以我写了一个脚本,启动 nuke像这样:

subprocess.call(["C:/Program Files/Nuke6.3v5/Nuke6.3", "-t", "E:/NukeTest/test.nk"])

然后什么都不会发生,因为 nuke正在等待用户输入。我现在如何写入标准输入?

我这样做是因为我正在运行一个带有 nuke的插件,它会在渲染多帧时间歇性崩溃。所以我希望这个脚本能够启动 nuke,告诉它做一些事情,然后如果它崩溃,再试一次。因此,如果有一种方法赶上一次坠机,仍然没有问题,那将是伟大的。

154851 次浏览

You can provide a file-like object to the stdin argument of subprocess.call().

The documentation for the Popen object applies here.

To capture the output, you should instead use subprocess.check_output(), which takes similar arguments. From the documentation:

>>> subprocess.check_output(
...     "ls non_existent_file; exit 0",
...     stderr=subprocess.STDOUT,
...     shell=True)
'ls: non_existent_file: No such file or directory\n'

It might be better to use communicate:

from subprocess import Popen, PIPE, STDOUT
p = Popen(['myapp'], stdout=PIPE, stdin=PIPE, stderr=PIPE)
stdout_data = p.communicate(input='data_to_write')[0]

"Better", because of this warning:

Use communicate() rather than .stdin.write, .stdout.read or .stderr.read to avoid deadlocks due to any of the other OS pipe buffers filling up and blocking the child process.

To clarify some points:

As jro has mentioned, the right way is to use subprocess.communicate.

Yet, when feeding the stdin using subprocess.communicate with input, you need to initiate the subprocess with stdin=subprocess.PIPE according to the docs.

Note that if you want to send data to the process’s stdin, you need to create the Popen object with stdin=PIPE. Similarly, to get anything other than None in the result tuple, you need to give stdout=PIPE and/or stderr=PIPE too.

Also qed has mentioned in the comments that for Python 3.4 you need to encode the string, meaning you need to pass Bytes to the input rather than a string. This is not entirely true. According to the docs, if the streams were opened in text mode, the input should be a string (source is the same page).

If streams were opened in text mode, input must be a string. Otherwise, it must be bytes.

So, if the streams were not opened explicitly in text mode, then something like below should work:

import subprocess
command = ['myapp', '--arg1', 'value_for_arg1']
p = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = p.communicate(input='some data'.encode())[0]

I've left the stderr value above deliberately as STDOUT as an example.

That being said, sometimes you might want the output of another process rather than building it up from scratch. Let's say you want to run the equivalent of echo -n 'CATCH\nme' | grep -i catch | wc -m. This should normally return the number characters in 'CATCH' plus a newline character, which results in 6. The point of the echo here is to feed the CATCH\nme data to grep. So we can feed the data to grep with stdin in the Python subprocess chain as a variable, and then pass the stdout as a PIPE to the wc process' stdin (in the meantime, get rid of the extra newline character):

import subprocess


what_to_catch = 'catch'
what_to_feed = 'CATCH\nme'


# We create the first subprocess, note that we need stdin=PIPE and stdout=PIPE
p1 = subprocess.Popen(['grep', '-i', what_to_catch], stdin=subprocess.PIPE, stdout=subprocess.PIPE)


# We immediately run the first subprocess and get the result
# Note that we encode the data, otherwise we'd get a TypeError
p1_out = p1.communicate(input=what_to_feed.encode())[0]


# Well the result includes an '\n' at the end,
# if we want to get rid of it in a VERY hacky way
p1_out = p1_out.decode().strip().encode()


# We create the second subprocess, note that we need stdin=PIPE
p2 = subprocess.Popen(['wc', '-m'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)


# We run the second subprocess feeding it with the first subprocess' output.
# We decode the output to convert to a string
# We still have a '\n', so we strip that out
output = p2.communicate(input=p1_out)[0].decode().strip()

This is somewhat different than the response here, where you pipe two processes directly without adding data directly in Python.

Hope that helps someone out.

Since subprocess 3.5, there is the subprocess.run() function, which provides a convenient way to initialize and interact with Popen() objects. run() takes an optional input argument, through which you can pass things to stdin (like you would using Popen.communicate(), but all in one go).

Adapting jro's example to use run() would look like:

import subprocess
p = subprocess.run(['myapp'], input='data_to_write', capture_output=True, text=True)

After execution, p will be a CompletedProcess object. By setting capture_output to True, we make available a p.stdout attribute which gives us access to the output, if we care about it. text=True tells it to work with regular strings rather than bytes. If you want, you might also add the argument check=True to make it throw an error if the exit status (accessible regardless via p.returncode) isn't 0.

This is the "modern"/quick and easy way to do to this.