Как мне записать в подпроцесс 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
, скажите ему, чтобы он что-то сделал, а затем, если произойдет сбой, попробуйте еще раз. Итак, если есть способ зафиксировать сбой и при этом все будет в порядке, то это было бы здорово.
Переведено автоматически
Ответ 1
Возможно, было бы лучше использовать communicate
:
from subprocess import Popen, PIPE, STDOUT
p = Popen(['myapp'], stdout=PIPE, stdin=PIPE, stderr=PIPE, text=True)
stdout_data = p.communicate(input='data_to_write')[0]
"Лучше" из-за этого предупреждения:
Используйте communicate() вместо .stdin.write, .stdout.read или .stderr.read, чтобы избежать взаимоблокировок из-за заполнения любого из других буферов канала ОС и блокирования дочернего процесса.
Ответ 2
Чтобы прояснить некоторые моменты:
Как jro упоминал, правильный способ - использовать subprocess.communicate
.
Тем не менее, при подаче stdin
using subprocess.communicate
с помощью input
вам необходимо инициировать подпроцесс с помощью stdin=subprocess.PIPE
согласно docs.
Обратите внимание, что если вы хотите отправить данные в stdin процесса, вам нужно создать объект Popen с помощью stdin=PIPE . Аналогично, чтобы получить что-либо, кроме None, в результирующем кортеже, вам также нужно указать stdout=PIPE и / или stderr=PIPE .
Также qed упомянул в комментариях, что для Python 3.4 вам нужно закодировать строку, что означает, что вам нужно передавать байты в input
, а не в string
. Это не совсем верно. Согласно документации, если потоки были открыты в текстовом режиме, вводом должна быть строка (источник - та же страница).
Если потоки были открыты в текстовом режиме, ввод должен быть строкой. В противном случае это должны быть байты.
Итак, если потоки не были открыты явно в текстовом режиме, то должно сработать что-то вроде приведенного ниже:
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]
Я намеренно оставил stderr
значение выше в качестве STDOUT
примера.
При этом иногда вам может потребоваться вывод другого процесса, а не создавать его с нуля. Допустим, вы хотите запустить эквивалент echo -n 'CATCH\nme' | grep -i catch | wc -m
. Обычно это должно возвращать символы числа в 'CATCH' плюс символ новой строки, что приводит к 6. Смысл echo здесь в том, чтобы передать CATCH\nme
данные в grep. Таким образом, мы можем передать данные в grep с помощью stdin в цепочке подпроцессов Python в качестве переменной, а затем передать стандартный вывод в виде КАНАЛА в wc
процесс' stdin (тем временем, избавьтесь от лишнего символа перевода строки):
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()
Это несколько отличается от ответа здесь, где вы передаете два процесса напрямую, не добавляя данные непосредственно в Python.
Надеюсь, это кому-нибудь поможет.
Ответ 3
Начиная с subprocess
3.5, существует subprocess.run()
функция, которая предоставляет удобный способ инициализации и взаимодействия с Popen()
объектами. run()
принимает необязательный input
аргумент, через который вы можете передавать данные stdin
(как вы бы использовали Popen.communicate()
, но все за один раз).
Адаптация примера jro для использования run()
будет выглядеть следующим образом:
import subprocess
p = subprocess.run(['myapp'], input='data_to_write', capture_output=True, text=True)
После выполнения p
будет CompletedProcess
объект. Устанавливая capture_output
значение True
, мы делаем доступным p.stdout
атрибут, который дает нам доступ к выходным данным, если мы заботимся об этом. text=True
сообщает ему, что он должен работать с обычными строками, а не с байтами. Если вы хотите, вы также можете добавить аргумент check=True
, чтобы он выдавал ошибку, если состояние выхода (доступное независимо через p.returncode
) не равно 0.
Это "современный" / быстрый и простой способ сделать это.
Ответ 4
хотя .communicate()
это рекомендуемый способ, он решает не все варианты использования. Для тех, кто хочет записать поток строк и прочитать его преобразование обратно из подпроцесса, вот пример кода.
import sys
from pathlib import Path
import itertools
import subprocess
import threading
def copy_to_stdin(proc, src_file: Path, mt_file: Path):
"""Example task: Write data to subproc stdin.
Note: run this on another thread to avoid deadlocks
This function reads two parallel files (src_file and mt_file), and write them as TSV record to the stdin of the sub process.
:param proc: subprocess object to write to
:param src_file: path to source file
:param mt_file: path to MT file
"""
with src_file.open() as src_lines, mt_file.open() as mt_lines:
for src_line, mt_line in itertools.zip_longest(src_lines, mt_lines):
if src_line is None or mt_line is None:
log.error(f'Input files have different number of lines')
raise ValueError('Input files have different number of lines')
line = src_line.rstrip('\n') + '\t' + mt_line.rstrip('\n') + '\n'
proc.stdin.write(line)
proc.stdin.flush()
proc.stdin.close() # close stdin to signal end of input
cmd_line = ['yourcmd', 'arg1'] # fill your args
src_file, mt_file = ... # your files
proc = subprocess.Popen(cmd_line, shell=False,
stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=sys.stderr, text=True, encoding='utf8', errors='replace')
try:
copy_thread = threading.Thread(target=copy_to_stdin, args=(proc, src_file, mt_file))
copy_thread.start()
# demonstration of reading data from stdout.
for line in proc.stdout:
line = line.rstrip()
print(line)
copy_thread.join()
returncode = proc.wait()
if returncode != 0:
raise RuntimeError(f'Process exited with code {returncode}')
finally:
if proc.returncode is None:
log.warning(f'Killing process {proc.pid}')
proc.kill()