it-swarm.com.de

Python: subprocess.call, stdout to file, stderr to file, stderr in Echtzeit auf dem Bildschirm anzeigen

Ich habe ein Befehlszeilentool (eigentlich mehrere), für das ich einen Wrapper in Python schreibe. 

Das Tool wird im Allgemeinen wie folgt verwendet:

 $ path_to_tool -option1 -option2 > file_out

Der Benutzer erhält die Ausgabe in file_out und kann auch verschiedene Statusmeldungen des Tools anzeigen, während es ausgeführt wird. 

Ich möchte dieses Verhalten replizieren und gleichzeitig stderr (Statusmeldungen) in eine Datei protokollieren. 

Was ich habe ist folgendes:

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

Dies funktioniert gut, außer dass stderr nicht auf den Bildschirm geschrieben wird. Ich kann natürlich Code hinzufügen, um den Inhalt der Protokolldatei auf dem Bildschirm auszudrucken, aber der Benutzer wird den Code dann sehen, wenn alles erledigt ist und nicht, während dies geschieht. 

Zusammenfassend ist das gewünschte Verhalten:

  1. call () oder subprozess () verwenden 
  2. direktes stdout zu einer Datei 
  3. stderr direkt in eine Datei schreiben, während stderr in Echtzeit auf den Bildschirm geschrieben wird, als wäre das -Tool direkt von der Befehlszeile aus aufgerufen worden.

Ich habe das Gefühl, dass mir entweder etwas sehr einfaches fehlt oder das ist viel komplizierter als ich dachte ... Danke für jede Hilfe!

EDIT: Dies muss nur unter Linux funktionieren.

25
Ben S.

Sie können dies mit subprocess tun , aber es ist nicht trivial. Wenn Sie sich Frequently Used Arguments in den Dokumenten ansehen, werden Sie feststellen, dass Sie PIPE als stderr - Argument übergeben können, wodurch eine neue Pipe erstellt wird , übergibt eine Seite der Pipe an den untergeordneten Prozess und stellt die andere Seite als Attribut stderr zur Verfügung. *

Daher müssen Sie diese Pipe warten und auf den Bildschirm und in die Datei schreiben. Im Allgemeinen ist es sehr schwierig, die richtigen Details dafür zu finden. ** In Ihrem Fall gibt es nur ein Rohr, und Sie planen, es synchron zu warten, also ist es nicht so schlimm.

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()

(Beachten Sie, dass es einige Probleme bei der Verwendung von for line in proc.stderr: Gibt. Wenn sich herausstellt, dass das, was Sie lesen, aus irgendeinem Grund nicht zeilengepuffert ist, können Sie auf eine neue Zeile warten, obwohl tatsächlich eine halbe Zeile vorhanden ist Sie können Blöcke gleichzeitig mit read(128) oder sogar read(1) lesen, um die Daten bei Bedarf reibungsloser abzurufen Jedes Byte, sobald es ankommt und sich die Kosten für read(1) nicht leisten können, müssen Sie die Pipe in den nicht blockierenden Modus versetzen und asynchron lesen.)


Wenn Sie jedoch unter Unix arbeiten, ist es möglicherweise einfacher, den Befehl tee für Sie zu verwenden.

Für eine schnelle und schmutzige Lösung können Sie die Shell verwenden, um durch sie zu leiten. Etwas wie das:

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

Aber ich möchte Shell-Piping nicht debuggen. Lass es uns in Python machen, wie gezeigt in den Dokumenten :

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()

Schließlich gibt es ein Dutzend oder mehr übergeordnete Wrapper für Unterprozesse und/oder die Shell auf PyPI - sh, Shell, Shell_command, shellout, iterpipes, sarge, cmd_utils, commandwrapper usw. Suchen Sie nach "Shell", "Subprozess", "Prozess", "Befehlszeile" usw und finden Sie eine, die Sie mögen, macht das Problem trivial.


Was ist, wenn Sie sowohl stderr als auch stdout sammeln müssen?

Wie Sven Marnach in einem Kommentar vorschlägt, können Sie dies ganz einfach tun, indem Sie einfach eine Umleitung auf die andere durchführen. Ändern Sie einfach die Parameter Popen wie folgt:

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

Und überall, wo Sie tool.stderr Verwendet haben, verwenden Sie stattdessen tool.stdout - beispielsweise für das letzte Beispiel:

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

Dies hat jedoch einige Nachteile. Am offensichtlichsten bedeutet das Mischen der beiden Streams, dass Sie stdout nicht in file_out und stderr nicht in log_file protokollieren oder stdout in Ihr stdout und stderr in Ihr stderr kopieren können. Dies bedeutet jedoch auch, dass die Reihenfolge nicht deterministisch sein kann. Wenn der Unterprozess immer zwei Zeilen in stderr schreibt, bevor etwas in stdout geschrieben wird, erhalten Sie möglicherweise eine Menge stdout zwischen diesen beiden Zeilen, sobald Sie die Streams mischen. Dies bedeutet, dass sie den Puffermodus von stdout gemeinsam nutzen müssen. Wenn Sie sich also auf die Tatsache verlassen, dass linux/glibc die Zeilenpufferung von stderr garantiert (es sei denn, der Unterprozess ändert dies explizit), ist dies möglicherweise nicht mehr der Fall.


Wenn Sie die beiden Prozesse getrennt behandeln müssen, wird es schwieriger. Früher habe ich gesagt, dass die schnelle Wartung des Rohrs einfach ist, solange Sie nur ein Rohr haben und es synchron warten können. Wenn Sie zwei Pfeifen haben, ist das offensichtlich nicht mehr wahr. Stellen Sie sich vor, Sie warten auf tool.stdout.read() und neue Daten kommen von tool.stderr. Wenn zu viele Daten vorhanden sind, kann dies zu einem Überlauf der Pipe und zu einem Blockieren des Unterprozesses führen. Aber selbst wenn dies nicht der Fall ist, können Sie die stderr-Daten offensichtlich erst dann lesen und protokollieren, wenn etwas von stdout eingeht.

Wenn Sie die Pipe-Through-Lösung tee verwenden, wird das anfängliche Problem umgangen, aber nur, indem Sie ein neues Projekt erstellen, das genauso schlecht ist. Sie haben zwei Instanzen tee, und während Sie auf einer Instanz communicate aufrufen, wartet die andere für immer.

In beiden Fällen benötigen Sie also einen asynchronen Mechanismus. Sie können dies mit Threads, einem select - Reaktor, so etwas wie gevent usw. tun.

Hier ist ein kurzes und schmutziges Beispiel:

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()

Es gibt jedoch einige Edge-Fälle, in denen dies nicht funktioniert. (Das Problem ist die Reihenfolge, in der SIGCHLD und SIGPIPE/EPIPE/EOF eintreffen. Ich glaube nicht, dass uns dies hier beeinflussen wird, da wir keine Eingaben senden ... aber vertrauen Sie mir nicht, ohne darüber nachzudenken durch und/oder testen.) Die subprocess.communicate Funktion von 3.3+ macht alle Details richtig. Möglicherweise ist es jedoch viel einfacher, eine der Async-Subprozess-Wrapper-Implementierungen zu verwenden, die Sie auf PyPI und ActiveState finden, oder sogar die Subprozess-Komponenten eines vollwertigen Async-Frameworks wie Twisted.


* Die Dokumentation erklärt nicht wirklich, was Pipes sind, fast so, als würden sie erwarten, dass Sie eine alte Unix-C-Hand sind ... Aber einige Beispiele, insbesondere in Ersetzen älterer Funktionen durch die subprocess Modul Abschnitt zeigen, wie sie verwendet werden, und es ist ziemlich einfach.

** Der schwierige Teil ist die ordnungsgemäße Sequenzierung von zwei oder mehr Rohren. Wenn Sie auf eine Leitung warten, kann die andere Leitung überlaufen und blockieren, wodurch verhindert wird, dass die Wartezeit auf die andere Leitung jemals endet. Die einzige einfache Möglichkeit, dies zu umgehen, besteht darin, ein Gewinde für die Wartung der einzelnen Rohre zu erstellen. (Auf den meisten * nix-Plattformen können Sie stattdessen einen select - oder poll -Reaktor verwenden, die plattformübergreifende Erstellung ist jedoch erstaunlich schwierig.) Die Quelle Das Modul, insbesondere communicate und seine Helfer, zeigen, wie es geht. (Ich habe auf 3.3 verlinkt, weil in früheren Versionen communicate selbst einige wichtige Dinge falsch macht ...) Aus diesem Grund möchten Sie nach Möglichkeit communicate verwenden, wenn Sie mehr als eine Pipe benötigen. In Ihrem Fall können Sie communicate nicht verwenden, aber zum Glück brauchen Sie nicht mehr als eine Pipe.

58
abarnert

Ich musste einige Änderungen an @ abarnerts Antwort für Python 3 vornehmen. Das scheint zu funktionieren:

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()
1
Timmmm

Ich denke, was Sie suchen, ist so etwas wie:

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

Um die Ausgabe/das Protokoll in eine Datei schreiben zu lassen, würde ich meine cmdline so ändern, dass sie die üblichen Weiterleitungen enthält, wie dies bei einer einfachen Linux-Bash/Shell der Fall wäre. Zum Beispiel würde ich tee an die Befehlszeile anhängen: cmdline += ' | tee -a logfile.txt'

Hoffentlich hilft das.

0
Brandt