Python: sous-processus.appel, stdout vers un fichier, stderr dans un fichier, afficher stderr sur l'écran en temps réel

j'ai un outil en ligne de commande (en fait, plusieurs) pour lequel j'écris un wrapper en Python.

L'outil est généralement utilisé comme ceci:

 $ path_to_tool -option1 -option2 > file_out

l'utilisateur obtient la sortie écrite à file_out, et est également capable de voir divers messages d'état de l'outil pendant qu'il exécute.

je veux répliquer ce comportement, tout en enregistrant stderr (les messages d'état) dans un fichier.

Ce que j'ai, c'est ceci:

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

Cela fonctionne très bien, sauf que stderr n'est pas écrit à l'écran. Je peux ajouter du code pour imprimer le contenu du fichier log_file à l'écran bien sûr, mais alors l'utilisateur le verra après que tout est fait plutôt que pendant qu'il se passe.

pour récapituler, le comportement désiré est:

  1. utiliser call(), ou sous-processus()
  2. dirigez stdout vers un fichier
  3. dirigez stderr vers un fichier, tout en écrivant stderr à l'écran en temps réel comme si le l'outil a été appelé directement depuis la ligne de commande.

j'ai le sentiment que je manque quelque chose de très simple, ou que c'est beaucoup plus compliqué que je ne le pensais...merci pour toute aide!

EDIT: cela ne doit fonctionner que sous Linux.

25
demandé sur Ben S. 2013-08-21 01:03:22
la source

3 ответов

Vous le faire avec subprocess, mais il n'est pas trivial. Si vous regardez l' Arguments Fréquemment Utilisés dans les docs, vous verrez que vous pouvez passer PIPEstderr argument, qui crée une nouvelle pipe, passe d'un côté de la pipe au procédé de l'enfant, et rend l'autre côté disponible à utiliser comme stderr l'attribut.*

ainsi, vous aurez besoin de réparer cette pipe, en écrivant à l'écran et au fichier. En général, les détails bon pour c'est très difficile.** Dans votre cas, il n'y a qu'un seul tuyau, et vous prévoyez l'entretenir de façon synchrone, donc ce n'est pas si mal.

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

(notez qu'il y a des problèmes avec for line in proc.stderr:-en gros, si ce que vous lisez s'avère ne pas être line-buffered pour quelque raison que ce soit, vous pouvez rester assis à attendre une nouvelle ligne, même s'il y a en fait une demi-ligne de données à traiter. Vous pouvez lire des morceaux à la fois avec, disons,read(128), ou encore read(1), pour obtenir le des données plus facilement si nécessaire. Si vous avez besoin d'obtenir effectivement chaque octet dès qu'il arrive, et ne peut pas se permettre le coût d' read(1), vous aurez besoin de mettre la pipe en mode non-bloquant et de lire asynchrone.)


Mais si vous êtes sous Unix, il pourrait être plus simple d'utiliser les tee commande de le faire pour vous.

pour une solution rapide et sale, vous pouvez utiliser la coque pour la pipe à travers elle. Quelque chose comme ceci:

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

Mais je ne veux pas shell de débogage de la tuyauterie; nous allons le faire en Python, comme le montre dans la doc:

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

enfin, il y a une douzaine ou plus d'enveloppes de niveau supérieur autour des sous-processus et / ou de l'interpréteur de commandes sur PyPI-sh,shell,shell_command,shellout,iterpipes,sarge,cmd_utils,commandwrapper, etc. Recherche pour "shell", "sous-processus", "processus", "ligne de commande", etc. et en trouver un qui te plaise rend le problème insignifiant.


Que faire si vous besoin de rassembler à la fois stderr et stdout?

la manière simple de le faire est de simplement rediriger l'un vers l'autre, comme Sven Marnach le suggère dans un commentaire. Il suffit de changer le Popen paramètres comme ceci:

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

Et puis partout vous avez utilisé tool.stderr, utilisez tool.stdout à la place-par exemple, pour le dernier exemple:

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

mais il y a quelques compromis. De toute évidence, mélanger les deux flux signifie que vous ne pouvez pas vous connecter à file_out et stderr à log_file, ou copiez stdout à votre stdout et stderr à votre stderr. Mais cela signifie aussi que l'ordre peut être non-déterministe-si le sous-processus écrit toujours deux lignes à stderr avant d'écrire quoi que ce soit à stdout, vous pourriez finir par obtenir un tas de stdout entre ces deux lignes une fois que vous mélangez les flux. Et cela signifie qu'ils doivent partager le mode de mise en tampon de stdout, donc si vous comptiez sur le fait que linux / glibc garantit à stderr d'être mis en tampon de ligne (à moins que le sous-processus ne le modifie explicitement), cela peut ne pas plus vrai.


si vous devez traiter les deux processus séparément, cela devient plus difficile. Plus tôt, j'ai dit que l'entretien de la pipe à la volée est facile Aussi longtemps que vous avez seulement un tuyau et pouvez l'entretien synchrone. Si vous avez deux tuyaux, ce n'est évidemment plus vrai. Imaginez que vous êtes en attente sur tool.stdout.read(), et de nouvelles données à partir de tool.stderr. S'il y a trop de données, cela peut causer le débordement du tuyau et le sous-processus à bloquer. Mais même si c' ce n'est pas le cas, vous ne pourrez évidemment pas lire et enregistrer les données de stderr jusqu'à ce que quelque chose arrive de stdout.

Si vous utilisez le tuyau de-grâce-tee solution, qui évite le problème initial... mais seulement en créant un nouveau projet qui est tout aussi mauvais. Vous avez deux tee les instances, et pendant que vous appelez communicate d'un côté, l'autre est assis à attendre éternellement.

Donc, de toute façon, vous avez besoin d'une sorte de mécanisme asynchrone. Vous pouvez le faire avec des fils, a select réacteur, quelque chose comme gevent, etc.

Voici un rapide exemple:

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

cependant, il y a des cas extrêmes où cela ne marchera pas. (Le problème est l'ordre D'arrivée de SIGCHLD et SIGPIPE/EPIPE/EOF. Je ne pense pas que cela nous affectera ici, puisque nous n'envoyons aucune contribution... mais ne me faites pas confiance pour ça sans y réfléchir et/ou le tester.)subprocess.communicate fonction à partir de 3.3 + obtient tous les délicats détails à droite. Mais vous pouvez trouver beaucoup plus simple d'utiliser l'une des implémentations de wrapper async-subprocess que vous pouvez trouver sur PyPI et ActiveState, ou même la substance de subprocess à partir d'un cadre async complet comme Twisted.


* les docs n'expliquent pas vraiment ce que sont les pipes, presque comme s'ils s'attendaient à ce que vous soyez une vieille main Unix C... mais quelques exemples, surtout dans le remplacer les anciennes fonctions par le subprocess Module section, montrer comment ils sont utilisés, et c'est assez simple.

** la partie dure est le séquençage de deux ou plusieurs pipes correctement. Si vous attendez sur un tuyau, l'autre peut déborder et de bloquer, empêcher votre attente sur l'autre de la finition. Le seul moyen facile de contourner cela est de créer un fil pour entretenir chaque tuyau. (Sur la plupart des plateformes * nix, vous pouvez utiliser un select ou poll réacteur à la place, mais rendre cette plateforme est étonnamment difficile.) la source pour le module, surtout communicate et ses assistants, montre comment le faire. (Je l'ai lié à 3.3, parce que dans les versions antérieures, communicate lui-même obtient quelques choses importantes mal...) C'est pourquoi, chaque fois que possible, vous voulez utiliser communicate si vous avez besoin de plus d'une pipe. Dans votre cas, vous ne pouvez pas utiliser communicate, mais heureusement, vous n'avez pas besoin de plus d'une pipe.

56
répondu abarnert 2013-08-22 21:36:35
la source

je pense que ce que vous recherchez est quelque chose comme:

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

Pour avoir la sortie/journal écrit dans un fichier, je voudrais modifier mon cmdline pour inclure d'habitude redirections, comme il le ferait sur une plaine linux bash/shell. Par exemple, je voudrais ajouter tee à la ligne de commande: cmdline += ' | tee -a logfile.txt'

j'Espère que vous aide.

0
répondu Brandt 2016-09-11 15:20:34
la source

j'ai dû apporter quelques changements à la réponse de @abarnert pour Python 3. Cela semble fonctionner:

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()
0
répondu Timmmm 2017-11-16 15:13:38
la source

Autres questions sur