Python Popen: записывать в файл stdout AND log одновременно

Я использую Popen для вызова оболочки script, которая постоянно записывает свои stdout и stderr в файл журнала. Есть ли способ одновременного вывода файла журнала непрерывно (на экран) или, альтернативно, сделать оболочку script одновременно записывать как файл журнала, так и stdout?

В основном я хочу сделать что-то подобное в Python:

cat file 2>&1 | tee -a logfile #"cat file" will be replaced with some script

Опять же, это трубы stderr/stdout вместе с tee, который записывает его как в stdout, так и в мой файл журнала.

Я знаю, как писать stdout и stderr в файле журнала в Python. Где я застрял, как дублировать их обратно на экран:

subprocess.Popen("cat file", shell=True, stdout=logfile, stderr=logfile)

Конечно, я мог бы просто сделать что-то подобное, но есть ли способ сделать это без перенаправления дескриптора файла tee и shell?:

subprocess.Popen("cat file 2>&1 | tee -a logfile", shell=True)

Ответ 1

Вы можете использовать канал для чтения данных из программы stdout и записывать ее во все нужные места:

import sys
import subprocess

logfile = open('logfile', 'w')
proc=subprocess.Popen(['cat', 'file'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in proc.stdout:
    sys.stdout.write(line)
    logfile.write(line)
proc.wait()

ОБНОВЛЕНИЕ

В python 3 параметр universal_newlines управляет использованием труб. Если False, труба читает return bytes объекты и может потребоваться декодировать (например, line.decode('utf-8')), чтобы получить строку. Если True, python выполняет декодирование для вас

Изменено в версии 3.3: Когда universal_newlines имеет значение True, класс использует кодировку locale.getpreferredencoding(False) вместо locale.getpreferredencoding(). Дополнительную информацию об этом изменении см. В классе io.TextIOWrapper.

Ответ 2

Чтобы эмулировать: subprocess.call("command 2>&1 | tee -a logfile", shell=True) без вызова команды tee:

#!/usr/bin/env python2
from subprocess import Popen, PIPE, STDOUT

p = Popen("command", stdout=PIPE, stderr=STDOUT, bufsize=1)
with p.stdout, open('logfile', 'ab') as file:
    for line in iter(p.stdout.readline, b''):
        print line,  #NOTE: the comma prevents duplicate newlines (softspace hack)
        file.write(line)
p.wait()

Чтобы устранить возможные проблемы буферизации (если выход задерживается), см. ссылки в Python: прочитайте потоковый ввод из subprocess.communicate().

Здесь версия Python 3:

#!/usr/bin/env python3
import sys
from subprocess import Popen, PIPE, STDOUT

with Popen("command", stdout=PIPE, stderr=STDOUT, bufsize=1) as p, \
     open('logfile', 'ab') as file:
    for line in p.stdout: # b'\n'-separated lines
        sys.stdout.buffer.write(line) # pass bytes as is
        file.write(line)

Ответ 3

Запись в терминал побайтово для интерактивных приложений

Этот метод записывает любые байты, которые он получает в стандартный вывод, что более точно имитирует поведение tee, особенно для интерактивных приложений.

main.py

#!/usr/bin/env python3
import os
import subprocess
import sys
with subprocess.Popen(sys.argv[1:], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc, \
        open('logfile.txt', 'bw') as logfile:
    while True:
        byte = proc.stdout.read(1)
        if byte:
            sys.stdout.buffer.write(byte)
            sys.stdout.flush()
            logfile.write(byte)
            # logfile.flush()
        else:
            break
exit_status = proc.returncode

sleep.py

#!/usr/bin/env python3
import sys
import time
for i in range(10):
    print(i)
    sys.stdout.flush()
    time.sleep(1)

Сначала мы можем сделать неинтерактивную проверку работоспособности:

./main.py ./sleep.py

И мы видим это в режиме реального времени.

Далее для интерактивного теста вы можете запустить:

./main.py bash

Затем набираемые символы сразу же появляются в терминале по мере их ввода, что очень важно для интерактивных приложений. Вот что происходит при запуске:

bash | tee logfile.txt

Кроме того, если вы хотите, чтобы вывод сразу отображался в выходном файле, вы также можете добавить:

logfile.flush()

но tee не делает этого, и я боюсь, что это убьет производительность. Вы можете легко проверить это с помощью:

tail -f logfile.txt

Смежный вопрос: прямой вывод команды подпроцесса

Протестировано на Ubuntu 18.04, Python 3.6.7.