2013-08-20 1 views
23

У меня есть инструмент командной строки (фактически, несколько), который я пишу обертки для Python.Python: subprocess.call, stdout в файл, stderr в файл, отображение stderr на экране в реальном времени

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

$ 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 не написано на экране. Я могу добавить код для печати содержимого лог-файла на экран, конечно, но тогда пользователь увидит его после того, как все будет сделано, а не пока оно происходит.

Напомним, желаемое поведение:

  1. использование вызова() или подпроцесс()
  2. прямой стандартный вывод в файл
  3. прямой Stderr в файл, а также написание STDERR на экран в реальном времени, как если бы инструмент был вызван непосредственно из командной строки.

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

EDIT: это должно работать только в Linux.

+0

ли ваш код должен работать на Windows (или других не POSIXy платформ)? Если нет, есть более простой ответ. – abarnert

+0

Этого не нужно! –

+0

Связанный: [Подпроцесс Python получает выход для детей в файл и терминал?] (Http://stackoverflow.com/q/4984428/4279) – jfs

ответ

52

Вы можете сделать это с subprocess, но это не тривиально. Если вы посмотрите на Frequently Used Arguments в документах, вы увидите, что вы можете передать 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: -в основном, если то, что вы читаете, оказывается, не быть буферизацией строк по любой причине, вы можете сидеть в ожидании новой строки, даже если есть на самом деле половина вы можете читать фрагменты за раз, скажем, 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, как показано на рисунке in the docs:

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 рука ... Но некоторые из примеров, особенно в разделе Replacing Older Functions with the subprocess Module, показать, как они используется, и это довольно просто.

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

+1

Очень полезно, спасибо. Вы имели в виду писать p1 и p2? –

+0

@ user2063292: Извините, это 'tool' и' tee'. После примера кода немного слишком близко. :) Спасибо, что поймали его. – abarnert

+0

Является ли '2 |' предполагаемым трубой stderr? Это не в оболочке POSIX. –

0

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

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

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

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

0

я должен был сделать несколько изменений @ 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()