Подтвердить что ты не робот

Python: subprocess.call, stdout в файл, stderr в файл, отображение stderr на экране в режиме реального времени

У меня есть инструмент командной строки (на самом деле, несколько), который я пишу для оболочки в Python.

Инструмент обычно используется следующим образом:

 $ path_to_tool -option1 -option2 > file_out

Пользователь получает вывод, записанный в file_out, и также может видеть различные сообщения о состоянии инструмента во время его работы.

Я хочу реплицировать это поведение, а также записывать stderr (сообщения о состоянии) в файл.

Что у меня есть:

from subprocess import call
call(['path_to_tool','-option1','option2'], stdout = file_out, stderr = log_file)

Это отлично работает, ИСКЛЮЧАЕТ, что stderr не записывается на экран. Я могу добавить код, чтобы, конечно, распечатать содержимое лог файла на экране, но тогда пользователь увидит его после того, как все будет сделано, а не пока оно происходит.

Чтобы повторить, желаемое поведение:

  • использовать вызов() или подпроцесс()
  • direct stdout в файл
  • direct stderr в файл, а также записывать stderr на экран в реальном времени, как если бы инструмент был вызван непосредственно из командной строки.

У меня есть чувство, что у меня либо отсутствует что-то действительно простое, либо это намного сложнее, чем я думал... спасибо за любую помощь!

EDIT: это нужно только для работы с Linux.

4b9b3361

Ответ 1

Вы можете сделать это с помощью subprocess, но это не тривиально. Если вы посмотрите на Часто используемые аргументы в документах, вы увидите, что вы можете передать PIPE в качестве аргумента stderr, который создает новый канал, передает одну сторону канала дочернему процессу и делает другую сторону доступной для использования в качестве атрибута stderr. *

Итак, вам нужно будет обслуживать этот канал, записывая его на экран и в файл. В общем, получение деталей для этого очень сложно. ** В вашем случае есть только одна труба, и вы планируете ее обслуживать синхронно, так что это не так уж плохо.

import subprocess
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
for line in proc.stderr:
    sys.stdout.write(line)
    log_file.write(line)
proc.wait()

(Обратите внимание, что есть некоторые проблемы, использующие for line in proc.stderr: -basically, если то, что вы читаете, по какой-либо причине оказывается не зависящим от строки, вы можете сидеть в ожидании новой строки, даже если на самом деле половина строки ценность данных для обработки. Вы можете читать фрагменты за раз, скажем, read(128) или даже read(1), чтобы получить данные более плавно, если это необходимо. Если вам нужно получить каждый байт, как только он появится, и не может позволить себе стоимость read(1), вам нужно будет поместить канал в неблокирующий режим и прочитать асинхронно.)


Но если вы работаете в Unix, проще было бы использовать команду tee, чтобы сделать это для вас.

Для быстрого и грязного решения вы можете использовать оболочку для ее прокрутки. Что-то вроде этого:

subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True,
                stdout=file_out)

Но я не хочу отлаживать оболочки оболочки; давайте сделаем это в Python, как показано в документах:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stderr)
tool.stderr.close()
tee.communicate()

Наконец, есть десятки или более обертки более высокого уровня вокруг подпроцессов и/или оболочки на PyPI- sh, shell, shell_command, shellout, iterpipes, sarge, cmd_utils, commandwrapper и т.д. Поиск "оболочки", "подпроцесса", "процесса", "командной строки" и т.д. и поиска того, что вам нравится, делает проблему тривиальной.


Что делать, если вам нужно собрать как stderr, так и stdout?

Легкий способ сделать это - просто перенаправить одно на другое, как предлагает Свен Марнах в комментарии. Просто измените параметры Popen следующим образом:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

И затем везде, где вы использовали tool.stderr, вместо tool.stdout используйте, например, для последнего примера:

tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout)
tool.stdout.close()
tee.communicate()

Но у этого есть некоторые компромиссы. Наиболее очевидно, что смешивание двух потоков вместе означает, что вы не можете записывать stdout в file_out и stderr в log_file или копировать stdout на ваш stdout и stderr на ваш stderr. Но это также означает, что упорядочение может быть недетерминированным - если подпроцесс всегда записывает две строки в stderr перед тем, как писать что-либо в stdout, вы можете получить кучу stdout между этими двумя строками, как только вы смешиваете потоки. И это означает, что они должны использовать режим буферизации stdout, поэтому, если вы полагаетесь на то, что linux/glibc гарантирует, что stderr будет буферизованным по строке (если подпроцесс явно не изменит его), это может быть больше недействительным.


Если вам нужно обрабатывать два процесса отдельно, это становится более сложным. Ранее я сказал, что обслуживание трубы "на лету" легко, пока у вас есть только одна труба и может обслуживать ее синхронно. Если у вас две трубы, это, очевидно, уже не так. Представьте, что вы ожидаете tool.stdout.read(), а новые данные поступают от tool.stderr. Если имеется слишком много данных, это может привести к переполнению канала и блокировке подпроцесса. Но даже если этого не произойдет, вы, очевидно, не сможете читать и записывать данные stderr до тех пор, пока что-то не поступит из stdout.

Если вы используете решение pipe-through- tee, которое позволяет избежать начальной проблемы... но только путем создания нового проекта, который так же плох. У вас есть два экземпляра tee, и пока вы вызываете communicate на одном, другой сидит в ожидании навсегда.

Итак, в любом случае вам нужен какой-то асинхронный механизм. Вы можете сделать это с помощью потоков, реактора select, что-то вроде gevent и т.д.

Вот быстрый и грязный пример:

proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)
t1 = threading.Thread(target=tee_pipe, args=(proc.stdout, file_out, sys.stdout))
t2 = threading.Thread(target=tee_pipe, args=(proc.stderr, log_file, sys.stderr))
t3 = threading.Thread(proc.wait)
t1.start(); t2.start(); t3.start()
t1.join(); t2.join(); t3.join()

Тем не менее, есть некоторые краевые случаи, когда это не сработает. (Проблема заключается в том, как поступают SIGCHLD и SIGPIPE/EPIPE/EOF. Я не думаю, что что-то из этого повлияет на нас здесь, так как мы не отправляем какие-либо данные... но не доверяйте мне, не думая об этом через и/или тестирование.) Функция subprocess.communicate из 3.3+ правильно описывает все детали. Но вы можете найти гораздо проще использовать одну из реализаций оболочки асинхронного подпроцесса, которую вы можете найти в PyPI и ActiveState, или даже материал подпроцесса из полноценной структуры async, такой как Twisted.


* Документы действительно не объясняют, что такое трубы, почти так, как если бы они ожидали, что вы старая ручка Unix C... Но некоторые примеры, особенно в Замена старых функций с помощью раздела subprocess Module, покажите, как они используются, и это довольно просто.

** Жесткая часть - это последовательность двух или более труб правильно. Если вы будете ждать на одной трубе, другая может переполняться и блокироваться, не дожидаясь вашего ожидания на другом, когда-либо заканчивая. Единственный простой способ обойти это - создать поток для обслуживания каждого канала. (На большинстве платформ nix вместо этого вы можете использовать реактор select или poll, но сделать эту кросс-платформу удивительно сложно.) Источник, особенно communicate и его помощники, показывает, как это сделать. (Я связан с 3.3, потому что в ранних версиях communicate сам по себе некоторые важные вещи ошибочны...) Вот почему, когда это возможно, вы хотите использовать communicate, если вам нужно больше одного канала. В вашем случае вы не можете использовать communicate, но, к счастью, вам не нужно больше одного канала.

Ответ 2

Я думаю, что то, что вы ищете, это что-то вроде:

import sys, subprocess
p = subprocess.Popen(cmdline,
                     stdout=sys.stdout,
                     stderr=sys.stderr)

Чтобы записать/записать файл в файл, я бы изменил свой cmdline, чтобы включить обычные перенаправления, как это было бы сделано на простой linux bash/shell. Например, я бы добавил tee в командной строке: cmdline += ' | tee -a logfile.txt'

Надеюсь, что это поможет.

Ответ 3

Мне пришлось внести несколько изменений в ответ @abarnert для Python 3. Это похоже на работу:

def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)

proc = subprocess.Popen(["/bin/echo", "hello"],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE)

# Open the output files for stdout/err in unbuffered mode.
out_file = open("stderr.log", "wb", 0)
err_file = open("stdout.log", "wb", 0)

stdout = sys.stdout
stderr = sys.stderr

# On Python3 these are wrapped with BufferedTextIO objects that we don't
# want.
if sys.version_info[0] >= 3:
    stdout = stdout.buffer
    stderr = stderr.buffer

# Start threads to duplicate the pipes.
out_thread = threading.Thread(target=tee_pipe,
                              args=(proc.stdout, out_file, stdout))
err_thread = threading.Thread(target=tee_pipe,
                              args=(proc.stderr, err_file, stderr))

out_thread.start()
err_thread.start()

# Wait for the command to finish.
proc.wait()

# Join the pipe threads.
out_thread.join()
err_thread.join()