Wie repliziere ich das Abschlagverhalten in Python, wenn ich einen Unterprozess verwende?

71

Ich suche nach einer Python-Lösung, mit der ich die Ausgabe eines Befehls in einer Datei speichern kann, ohne sie vor der Konsole zu verbergen.

Zu Ihrer Information : Ich frage nach tee (als Unix-Befehlszeilenprogramm) und nicht nach der gleichnamigen Funktion aus dem Python-Intertools-Modul.

Einzelheiten

  • Python-Lösung (nicht aufrufend tee, unter Windows nicht verfügbar)
  • Ich muss keine Eingabe für stdin für den aufgerufenen Prozess bereitstellen
  • Ich habe keine Kontrolle über das aufgerufene Programm. Ich weiß nur, dass es etwas an stdout und stderr ausgibt und mit einem Exit-Code zurückgibt.
  • Zum Aufrufen externer Programme (Unterprozess)
  • Für beide arbeiten stderrundstdout
  • In der Lage zu sein, zwischen stdout und stderr zu unterscheiden, weil ich möglicherweise nur eines davon auf der Konsole anzeigen möchte oder versuchen könnte, stderr mit einer anderen Farbe auszugeben - dies bedeutet, dass dies stderr = subprocess.STDOUTnicht funktioniert.
  • Live-Ausgabe (progressiv) - Der Prozess kann lange dauern und ich kann es kaum erwarten, bis er abgeschlossen ist.
  • Python 3-kompatibler Code (wichtig)

Verweise

Hier sind einige unvollständige Lösungen, die ich bisher gefunden habe:

Diagramm http://blog.i18n.ro/wp-content/uploads/2010/06/Drawing_tee_py.png

Aktueller Code (zweiter Versuch)

#!/usr/bin/python
from __future__ import print_function

import sys, os, time, subprocess, io, threading
cmd = "python -E test_output.py"

from threading import Thread
class StreamThread ( Thread ):
    def __init__(self, buffer):
        Thread.__init__(self)
        self.buffer = buffer
    def run ( self ):
        while 1:
            line = self.buffer.readline()
            print(line,end="")
            sys.stdout.flush()
            if line == '':
                break

proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdoutThread = StreamThread(io.TextIOWrapper(proc.stdout))
stderrThread = StreamThread(io.TextIOWrapper(proc.stderr))
stdoutThread.start()
stderrThread.start()
proc.communicate()
stdoutThread.join()
stderrThread.join()

print("--done--")

#### test_output.py ####

#!/usr/bin/python
from __future__ import print_function
import sys, os, time

for i in range(0, 10):
    if i%2:
        print("stderr %s" % i, file=sys.stderr)
    else:
        print("stdout %s" % i, file=sys.stdout)
    time.sleep(0.1)
Echte Ausgabe
stderr 1
stdout 0
stderr 3
stdout 2
stderr 5
stdout 4
stderr 7
stdout 6
stderr 9
stdout 8
--done--

Bei der erwarteten Ausgabe sollten die Zeilen bestellt werden. Bemerkung, das Ändern des Popen, um nur ein PIPE zu verwenden, ist nicht erlaubt, da ich im wirklichen Leben verschiedene Dinge mit stderr und stdout machen möchte.

Auch im zweiten Fall war ich nicht in der Lage, Echtzeit-Outs zu erhalten. Tatsächlich gingen alle Ergebnisse ein, als der Prozess abgeschlossen war. Standardmäßig sollte Popen keine Puffer verwenden (bufsize = 0).

Sorin
quelle

Antworten:

15

Ich sehe, dass dies ein ziemlich alter Beitrag ist, aber nur für den Fall, dass noch jemand nach einer Möglichkeit sucht, dies zu tun:

proc = subprocess.Popen(["ping", "localhost"], 
                        stdout=subprocess.PIPE, 
                        stderr=subprocess.PIPE)

with open("logfile.txt", "w") as log_file:
  while proc.poll() is None:
     line = proc.stderr.readline()
     if line:
        print "err: " + line.strip()
        log_file.write(line)
     line = proc.stdout.readline()
     if line:
        print "out: " + line.strip()
        log_file.write(line)
Benutzer1557760
quelle
Dies funktionierte für mich, obwohl ich es stdout, stderr = proc.communicate()einfacher zu bedienen fand.
Chase Seibert
20
-1: Diese Lösung führt zu einem Deadlock für jeden Unterprozess, der genügend Ausgabe auf stdout oder stderr erzeugen kann und bei dem stdout / stderr nicht perfekt synchron sind.
JFS
@JFSebastian: Stimmt, aber Sie können dieses Problem umgehen , indem Sie ersetzen readline()mit readline(size). Ich habe etwas Ähnliches in anderen Sprachen gemacht. Ref: docs.python.org/3/library/io.html#io.TextIOBase.readline
kevinarpe
5
@ Kevinarpe falsch. readline(size)wird den Deadlock nicht beheben. stdout / stderr sollte gleichzeitig gelesen werden. Siehe Links unter der Frage, die Lösungen mit Threads oder Asyncio anzeigen.
JFS
@JFSebastian Gibt es dieses Problem, wenn ich nur einen der Streams lesen möchte?
ThorSummoner
7

Dies ist eine einfache Portierung von teePython.

import sys
sinks = sys.argv[1:]
sinks = [open(sink, "w") for sink in sinks]
sinks.append(sys.stderr)
while True:
  input = sys.stdin.read(1024)
  if input:
    for sink in sinks:
      sink.write(input)
  else:
    break

Ich laufe gerade unter Linux, aber das sollte auf den meisten Plattformen funktionieren.


Nun zum subprocessTeil, weiß ich nicht , wie Sie mit ‚Draht‘ wollen die subprocess des stdin, stdoutund stderrzu Ihrer stdin, stdout, stderrund Datei sinkt, aber ich weiß, dies zu tun:

import subprocess
callee = subprocess.Popen( ["python", "-i"],
                           stdin = subprocess.PIPE,
                           stdout = subprocess.PIPE,
                           stderr = subprocess.PIPE
                         )

Jetzt können Sie zugreifen callee.stdin, callee.stdoutund callee.stderrwie normale Dateien, so dass die obige „Lösung“ zu arbeiten. Wenn Sie das erhalten möchten callee.returncode, müssen Sie einen zusätzlichen Anruf tätigen callee.poll().

Seien Sie vorsichtig beim Schreiben an callee.stdin: Wenn der Prozess dabei beendet wurde, kann ein Fehler auftreten (unter Linux verstehe ich IOError: [Errno 32] Broken pipe).

badp
quelle
2
Dies ist unter Linux nicht optimal, da Linux eine Ad-hoc- tee(f_in, f_out, len, flags)API bereitstellt, aber das ist nicht der Punkt, oder?
Badp
1
Ich habe die Frage aktualisiert. Das Problem ist, dass ich nicht herausfinden konnte, wie der Unterprozess verwendet werden kann, um die Daten aus den beiden Pipes schrittweise und am Ende des Prozesses nicht auf einmal abzurufen.
Sorin
Ich weiß, dass Ihr Code funktionieren sollte, aber es gibt eine kleine Anforderung, die die gesamte Logik verletzt: Ich möchte in der Lage sein, zwischen stdout und stderr zu unterscheiden, und dies bedeutet, dass ich von beiden lesen muss, aber ich weiß nicht, welcher Wille neue Daten erhalten. Bitte schauen Sie sich den Beispielcode an.
Sorin
1
@ Sorin, das heißt, Sie müssen entweder zwei Threads verwenden. Man liest weiter stdout, man liest weiter stderr. Wenn Sie beide in dieselbe Datei schreiben möchten, können Sie eine Sperre für die Senken erwerben, wenn Sie mit dem Lesen beginnen, und diese nach dem Schreiben eines Zeilenabschlusses freigeben. : /
Badp
Die Verwendung von Threads klingt für mich nicht besonders ansprechend, vielleicht finden wir etwas anderes. Es ist seltsam, dass dies ein häufiges Problem ist, aber niemand hat eine vollständige Lösung dafür bereitgestellt.
Sorin
5

So kann es gemacht werden

import sys
from subprocess import Popen, PIPE

with open('log.log', 'w') as log:
    proc = Popen(["ping", "google.com"], stdout=PIPE, encoding='utf-8')
    while proc.poll() is None:
        text = proc.stdout.readline() 
        log.write(text)
        sys.stdout.write(text)
Danylo Zhydyk
quelle
2
Für alle, die sich fragen, JA können Sie print()anstelle von verwenden sys.stdout.write(). :-)
Programmierer
@progyammer printfügt eine zusätzliche neue Zeile hinzu, die nicht Ihren Wünschen entspricht, wenn Sie die Ausgabe originalgetreu wiedergeben möchten.
ivan_pozdeev
Ja, aber print(line, end='')könnte das Problem lösen
Danylo Zhydyk
5

Wenn das Erfordernis von Python 3.6 kein Problem darstellt, gibt es jetzt eine Möglichkeit, dies mit Asyncio zu tun. Mit dieser Methode können Sie stdout und stderr getrennt erfassen, aber beide Streams ohne Verwendung von Threads zum tty übertragen. Hier ist eine grobe Übersicht:

class RunOutput():
    def __init__(self, returncode, stdout, stderr):
        self.returncode = returncode
        self.stdout = stdout
        self.stderr = stderr

async def _read_stream(stream, callback):
    while True:
        line = await stream.readline()
        if line:
            callback(line)
        else:
            break

async def _stream_subprocess(cmd, stdin=None, quiet=False, echo=False) -> RunOutput:
    if isWindows():
        platform_settings = {'env': os.environ}
    else:
        platform_settings = {'executable': '/bin/bash'}

    if echo:
        print(cmd)

    p = await asyncio.create_subprocess_shell(cmd,
                                              stdin=stdin,
                                              stdout=asyncio.subprocess.PIPE,
                                              stderr=asyncio.subprocess.PIPE,
                                              **platform_settings)
    out = []
    err = []

    def tee(line, sink, pipe, label=""):
        line = line.decode('utf-8').rstrip()
        sink.append(line)
        if not quiet:
            print(label, line, file=pipe)

    await asyncio.wait([
        _read_stream(p.stdout, lambda l: tee(l, out, sys.stdout)),
        _read_stream(p.stderr, lambda l: tee(l, err, sys.stderr, label="ERR:")),
    ])

    return RunOutput(await p.wait(), out, err)


def run(cmd, stdin=None, quiet=False, echo=False) -> RunOutput:
    loop = asyncio.get_event_loop()
    result = loop.run_until_complete(
        _stream_subprocess(cmd, stdin=stdin, quiet=quiet, echo=echo)
    )

    return result

Der obige Code basiert auf diesem Blog-Beitrag: https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/

Kalebo
quelle
1

Wenn Sie nicht mit dem Prozess interagieren möchten, können Sie das Unterprozessmodul problemlos verwenden.

Beispiel:

tester.py

import os
import sys

for file in os.listdir('.'):
    print file

sys.stderr.write("Oh noes, a shrubbery!")
sys.stderr.flush()
sys.stderr.close()

testing.py

import subprocess

p = subprocess.Popen(['python', 'tester.py'], stdout=subprocess.PIPE,
                     stdin=subprocess.PIPE, stderr=subprocess.PIPE)

stdout, stderr = p.communicate()
print stdout, stderr

In Ihrer Situation können Sie einfach zuerst stdout / stderr in eine Datei schreiben. Sie können Argumente auch mit communic an Ihren Prozess senden, obwohl ich nicht herausfinden konnte, wie ich kontinuierlich mit dem Unterprozess interagieren kann.

Wayne Werner
quelle
2
Dies zeigt Ihnen keine Fehlermeldungen in STDERR im Kontext von STDOUT an, was das Debuggen von Shell-Skripten usw. nahezu unmöglich machen kann.
RobM
Bedeutung...? In diesem Skript wird alles, was über STDERR geliefert wird, zusammen mit STDOUT auf dem Bildschirm gedruckt. Wenn Sie sich auf Rückkehrcodes beziehen, verwenden Sie p.poll()diese einfach, um sie abzurufen.
Wayne Werner
1
Dies erfüllt nicht die "progressive" Bedingung.
ivan_pozdeev
-1

Versuche dies :

import sys

class tee-function :

    def __init__(self, _var1, _var2) :

        self.var1 = _var1
        self.var2 = _var2

    def __del__(self) :

        if self.var1 != sys.stdout and self.var1 != sys.stderr :
            self.var1.close()
        if self.var2 != sys.stdout and self.var2 != sys.stderr :
            self.var2.close()

    def write(self, text) :

        self.var1.write(text)
        self.var2.write(text)

    def flush(self) :

        self.var1.flush()
        self.var2.flush()

stderrsav = sys.stderr

out = open(log, "w")

sys.stderr = tee-function(stderrsav, out)
Catalin Festila
quelle
Dies ist genau der Ansatz, den ich vorschlagen wollte. Es lohnt sich auch, einige der Dateidaten-Deskriptoren hinzuzufügen, wie z closed.
RobM
3
Ich habe es einfach versucht, subprocess.Popenrufe an fileno()und löse eine Ausnahme aus.
RobM
-1

Meine Lösung ist nicht elegant, aber sie funktioniert.

Sie können Powershell verwenden, um unter WinOS Zugriff auf "Tee" zu erhalten.

import subprocess
import sys

cmd = ['powershell', 'ping', 'google.com', '|', 'tee', '-a', 'log.txt']

if 'darwin' in sys.platform:
    cmd.remove('powershell')

p = subprocess.Popen(cmd)
p.wait()
Danylo Zhydyk
quelle
Gibt eine ungültige Befehlszeilenfehlermeldung unter pingMacOS aus.
ivan_pozdeev