web-dev-qa-db-fra.com

Comment exécuter plusieurs commandes de manière synchrone à partir d'une commande subprocess.Popen?

Est-il possible d'exécuter un nombre arbitraire de commandes en séquence en utilisant la même commande de sous-processus?

J'ai besoin que chaque commande attende la fin de la précédente avant de l'exécuter et j'ai besoin qu'elles soient toutes exécutées dans la même session/Shell. J'ai aussi besoin que cela fonctionne dans Python 2.6, Python 3.5. J'ai également besoin de la commande subprocess pour fonctionner sous Linux, Windows et macOS (c'est pourquoi je n'utilise que des commandes echo comme exemples ici).

Voir non-working code ci-dessous:

import sys
import subprocess

cmds = ['echo start', 'echo mid', 'echo end']

p = subprocess.Popen(cmd=Tuple([item for item in cmds]),
                     stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    sys.stdout.flush()
    print(">>> " + line.rstrip())

Si ce n'est pas possible, quelle approche dois-je adopter pour exécuter mes commandes en séquence synchrone au sein de la même session/du même shell?

7
fredrik

Si vous voulez exécuter plusieurs commandes les unes après les autres dans la même session session/Shell , vous devez démarrer un shell et l’alimenter avec toutes les commandes, l’une après l’autre, suivie d’une nouvelle ligne, et fermer le tuyau à la fin. Il est logique que certaines commandes ne soient pas de véritables processus, mais des commandes Shell susceptibles, par exemple, de modifier l'environnement Shell.

Exemple d'utilisation de Python 2.7 sous Windows:

encoding = 'latin1'
p = subprocess.Popen('cmd.exe', stdin=subprocess.PIPE,
             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for cmd in cmds:
    p.stdin.write(cmd + "\n")
p.stdin.close()
print p.stdout.read()

Pour que ce code soit exécuté sous Linux, vous devez remplacer cmd.exe par /bin/bash et probablement modifier le codage en utf8. 

Pour Python 3, vous devez encoder les commandes et probablement décoder leur sortie, et utiliser des parenthèses avec print.

Attention: cela ne peut fonctionner que pour un faible rendement. S'il y avait suffisamment de sortie pour remplir le tampon de canal avant de fermer le canal stdin, ce code se bloquerait. Un moyen plus robuste serait d'avoir un deuxième thread pour lire le résultat des commandes afin d'éviter ce problème.

4
Serge Ballesta

Une solution possible ressemble à son exécution dans le même shell:

subprocess.Popen('echo start;echo mid;echo end', Shell=True)

Remarque - Si vous transmettez votre commande sous forme de chaîne, Shell doit alors avoir la valeur True Remarque - Cela fonctionne uniquement sous Linux, vous devrez peut-être trouver un moyen similaire de sortir des fenêtres. 

J'espère que ça va aider.

De python doc - 

Sous Unix avec Shell = True, le shell par défaut est/bin/sh. Si args est un chaîne, la chaîne spécifie la commande à exécuter via le shell . Cela signifie que la chaîne doit être formatée exactement comme elle le serait lorsqu’il est tapé à l’invite du shell.

3
AlokThakur

Ceci est similaire à la réponse publiée par Serge Ballesta, mais pas tout à fait. Utilisez his pour l'exécution asynchrone, où vous ne vous souciez pas des résultats. Utilisez mine pour le traitement synchrone et la collecte des résultats. Comme sa réponse, je montre ici la solution Windows: exécuter un processus bash sous Linux plutôt que cmd sous Windows.

from subprocess import Popen, PIPE
process = Popen( "cmd.exe", Shell=False, universal_newlines=True,
                  stdin=PIPE, stdout=PIPE, stderr=PIPE )                             
out, err = process.communicate( commands ) 

DÉTAILS SUR L'UTILISATION: L'argument commands qui est passé ici à la méthode process.communicate est une newline delimited string. Si, par exemple, vous venez de lire le contenu d'un fichier de commandes dans une chaîne, vous pouvez l'exécuter de cette façon car il contiendrait déjà les nouvelles lignes. Important : votre chaîne doit se terminer par un newline "\n". Si ce n'est pas le cas, la dernière commande échouera. C'est comme si vous l'aviez tapé dans l'invite de commande mais que vous n'aviez pas appuyé sur enter à la fin. Vous verrez cependant une mystérieuse ligne More? à la fin de la sortie standard renvoyée. (c'est la cause si vous rencontrez cela).

process.communicate fonctionne par définition de manière synchrone et renvoie les messages stdout et stderr (si vous les avez dirigés vers subprocess.PIPE dans votre constructeur Popen).

Lorsque vous créez un processus cmd.exe de cette manière et que vous lui transmettez une chaîne, les résultats seront exactement comme si vous ouvriez une fenêtre d'invite de commande dans laquelle vous entrez des commandes. Et je le pense littéralement. Si vous testez cela, vous verrez que la commande standard qui est renvoyée contient vos commandes. (Peu importe si vous incluez un @echo off comme si vous exécutiez un fichier de traitement par lots).

Conseils pour ceux qui se soucient des résultats "propres" de la sortie standard: 

  • @echo off n'empêchera pas vos commandes d'apparaître dans cette chaîne renvoyée, mais supprimera les retours à la ligne supplémentaires qui s'y trouvent autrement. (universal_newlines = True supprime un autre ensemble de ceux-ci)

  • L'inclusion d'un préfixe de symbole @ dans vos commandes leur permet de continuer à s'exécuter. Dans un traitement par lots "normal", c'est le moyen, ligne par ligne, de "masquer" vos commandes. Dans ce contexte, c’est un marqueur facile à utiliser qui vous permet de trouver les lignes standard que vous souhaitez supprimer. (si on était si enclin)

  • Le "header" cmd.exe apparaîtra dans votre sortie (qui indique la version de Windows, etc.). Puisque vous voulez probablement commencer votre ensemble de commandes avec @echo off, pour couper les nouvelles lignes supplémentaires, c’est également un excellent moyen de localiser l’arrêt des lignes d’en-tête et le début de vos commandes/résultats.

Enfin, pour répondre aux préoccupations concernant les sorties "volumineuses" qui remplissent les tuyaux et vous causent des problèmes (d’abord, je pense que vous avez besoin d’une énorme quantité de données qui reviennent pour que cela soit un problème), bien au-delà de ce que la plupart des gens rencontreront dans leurs cas d’utilisation. Deuxièmement, s'il s'agit vraiment d'un problème, il suffit d'ouvrir un fichier en écriture et de transmettre ce descripteur de fichier (la référence à l'objet fichier) à stdout/err au lieu de PIPE. Ensuite, faites ce que vous voulez avec le fichier que vous avez créé.

2
BuvinJ

Celui-ci fonctionne en python 2.7 et devrait fonctionner également dans les fenêtres. Un petit raffinement est probablement nécessaire pour les pythons> 3. 

La sortie produite est (en utilisant date et veille, il est facile de voir que les commandes sont exécutées en ligne):

>>>Die Sep 27 12:47:52 CEST 2016
>>>
>>>Die Sep 27 12:47:54 CEST 2016

Comme vous voyez les commandes sont exécutées dans une rangée. 

    import sys
    import subprocess
    import shlex

    cmds = ['date', 'sleep 2', 'date']

    cmds = [shlex.split(x) for x in cmds]

    outputs =[]
    for cmd in cmds:
        outputs.append(subprocess.Popen(cmd,
                                   stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate())


    for line in outputs:
        print ">>>" + line[0].strip()

C'est ce que j'obtiens en fusionnant avec la réponse de @Marichyasana:

import sys
import os


def run_win_cmds(cmds):

    @Marichyasana code (+/-)

def run_unix_cmds(cmds):

    import subprocess
    import shlex


    cmds = [shlex.split(x) for x in cmds]

    outputs =[]
    for cmd in cmds:
        outputs.append(subprocess.Popen(cmd,
                                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate())


    rc = ''
    for line in outputs:
        rc +=  line[0].strip()+'\n'

    return rc


cmds = ['date', 'sleep 2', 'date']

if os.name == 'nt':
     run_win_cmds(cmds)
Elif os.name == 'posix':
    run_unix_cmds(cmds)

Demandez si celui-ci ne correspond pas à vos besoins! ;)

1
Riccardo Petraglia

Voici une fonction (et principale pour l'exécuter) que j'utilise. Je dirais que vous pouvez l'utiliser pour votre problème. Et c'est flexible.

# processJobsInAList.py
# 2016-09-27   7:00:00 AM   Central Daylight Time 

import win32process, win32event

def CreateMyProcess2(cmd):
    ''' create process width no window that runs a command with arguments
    and returns the process handle'''
    si   = win32process.STARTUPINFO()
    info = win32process.CreateProcess(
        None,      # AppName
        cmd,       # Command line
        None,      # Process Security
        None,      # Thread Security
        0,         # inherit Handles?
        win32process.NORMAL_PRIORITY_CLASS,
        None,      # New environment
        None,      # Current directory
        si)        # startup info
    # info is Tuple (hProcess, hThread, processId, threadId)
    return info[0]

if __== '__main__' :
    ''' create/run a process for each list element in "cmds"
    output may be out of order because processes run concurrently '''

    cmds=["echo my","echo heart","echo belongs","echo to","echo daddy"]
    handles    = []
    for i in range(len(cmds)):
        cmd    = 'cmd /c ' + cmds[i]
        handle = CreateMyProcess2(cmd)
        handles.append(handle)

    rc = win32event.WaitForMultipleObjects( handles, 1, -1)  # 1 wait for all, -1 wait infinite
    print 'return code ',rc

sortie:
cœur
mon
fait parti
à
papa
code retour 0 

UPDATE: Si vous voulez exécuter le même processus, ce qui va sérialiser les choses pour vous:
1) Supprimer la ligne: handles.append (handle)
2) Substituez la variable "handle" à la place de la liste "handles" sur la ligne "WaitFor"
3) Remplacez WaitForSingleObject à la place de WaitForMultipleObjects

1
Marichyasana