3 votes

Comment créer une méthode générique en Python pour exécuter plusieurs commandes shell pipées ?

J'ai de nombreuses commandes shell qui doivent être exécutées dans mon script python. Je sais que je ne dois pas utiliser shell=true, comme indiqué. aquí et que je peux utiliser les sorties et les entrées std dans le cas où j'ai des tuyaux dans la commande comme mentionné ci-dessus. aquí .

Mais le problème est que mes commandes shell sont complexes et pleines de tuyaux, donc j'aimerais faire une méthode générique qui serait utilisée par mon script.

J'ai fait un petit test ci-dessous, mais il est suspendu après l'impression du résultat (j'ai simplifié juste pour mettre ici). Quelqu'un peut-il me renseigner ?

  1. Pourquoi la pendaison.
  2. S'il y a une meilleure méthode pour le faire.

Merci.

PS : Ce n'est qu'une petite partie d'un grand projet python et il y a des raisons professionnelles pour lesquelles j'essaie de faire cela. Merci.

#!/usr/bin/env python3
import subprocess as sub
from subprocess import Popen, PIPE
import shlex

def exec_cmd(cmd,p=None,isFirstLoop=True):
   if not isFirstLoop and not p:
       print("Error, p is null")
       exit()
   if "|" in cmd:
       cmds = cmd.split("|")
       while "|" in cmd:
           # separates what is before and what is after the first pipe
           now_cmd = cmd.split('|',1)[0].strip()
           next_cmd = cmd.split('|',1)[-1].strip()
           try:
               if isFirstLoop:
                   p1 = sub.Popen(shlex.split(now_cmd), stdout=PIPE)
                   exec_cmd(next_cmd,p1,False)
               else:
                   p2 = sub.Popen(shlex.split(now_cmd),stdin=p.stdout, stdout=PIPE)
                   exec_cmd(next_cmd,p2,False)
           except Exception as e:
               print("Error executing command '{0}'.\nOutput:\n:{1}".format(cmd,str(e)))
               exit()
           # Adjust cmd to execute the next part
           cmd = next_cmd
   else:
       proc = sub.Popen(shlex.split(cmd),stdin=p.stdout, stdout=PIPE, universal_newlines=True)
       (out,err) = proc.communicate()
       if err:
           print(str(err).strip())
       else:
           print(out)

exec_cmd("ls -ltrh | awk '{print $9}' | wc -l ")

3voto

Mathias Ettinger Points 2888

Au lieu d'utiliser une chaîne shell et d'essayer de l'analyser avec vos propres moyens, je demanderais à l'utilisateur de fournir lui-même les commandes en tant qu'entités séparées. Cela évite le piège évident de la détection d'un | qui fait partie d'une commande et n'est pas utilisé comme un tuyau de l'interpréteur de commandes. Que vous leur demandiez de fournir des commandes sous la forme d'une liste de chaînes ou d'une chaîne unique que vous allez shlex.split Après, c'est à l'interface que vous voulez exposer. Je choisirais la première pour sa simplicité dans l'exemple suivant.

Une fois que vous avez les commandes individuelles, une simple for est suffisante pour acheminer les sorties des commandes précédentes vers les entrées des commandes suivantes, comme suit vous vous êtes trouvé :

def pipe_subprocesses(*commands):
    if not commands:
        return

    next_input = None
    for command in commands:
        p = subprocess.Popen(command, stdin=next_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        next_input = p.stdout

    out, err = p.communicate()
    if err:
        print(err.decode().strip())
    else:
        print(out.decode())

L'usage étant :

>>> pipe_subprocesses(['ls', '-lhtr'], ['awk', '{print $9}'], ['wc', '-l'])
25

Il s'agit d'un moyen rapide et sale de le configurer et de le faire fonctionner apparemment comme vous le souhaitez. Mais il y a au moins deux problèmes avec ce code :

  1. Vous faites fuir les processus zombies/manipulations de processus ouverts parce que le code de sortie de tous les processus, à l'exception du dernier, est collecté ; et le système d'exploitation garde les ressources ouvertes pour que vous puissiez le faire ;
  2. Vous ne pouvez pas accéder aux informations d'un processus qui échouerait à mi-parcours.

Pour éviter cela, vous devez maintenir une liste de processus ouverts et explicitement wait pour chacun d'entre eux. Et comme je ne connais pas votre cas d'utilisation exact, je renverrai simplement le premier processus qui a échoué (le cas échéant) ou le dernier processus (dans le cas contraire) afin que vous puissiez agir en conséquence :

def pipe_subprocesses(*commands):
    if not commands:
        return

    processes = []
    next_input = None
    for command in commands:
        if isinstance(command, str):
            command = shlex.split(command)
        p = subprocess.Popen(command, stdin=next_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        next_input = p.stdout
        processes.append(p)

    for p in processes:
        p.wait()

    for p in processes:
        if p.returncode != 0:
            return p
    return p  # return the last process in case everything went well

J'ai aussi mis un peu de shlex comme exemple afin que vous puissiez mélanger des chaînes brutes et des listes déjà analysées :

>>> pipe_subprocesses('ls -lhtr', ['awk', '{print $9}'], 'wc -l')
25

3voto

Dietrich Epp Points 72865

Malheureusement, il y a quelques cas limites que l'interpréteur de commandes prend en charge pour vous, ou qu'il ignore complètement pour vous. Quelques préoccupations :

  • La fonction doit toujours wait() pour que chaque processus se termine, sinon vous obtiendrez ce qu'on appelle des les processus zombies .

  • Les commandes doivent être connectées les unes aux autres à l'aide de vrais tuyaux, de cette façon, toute la sortie n'a pas besoin d'être lue en mémoire en une seule fois. C'est la façon normale dont les tuyaux fonctionnent.

  • L'extrémité de lecture de chaque tuyau doit être fermée dans le processus parent, afin que les enfants puissent correctement SIGPIPE lorsque le processus suivant ferme son entrée. Sans cela, le processus parent peut garder le tuyau ouvert et l'enfant ne sait pas qu'il doit sortir, et il peut fonctionner indéfiniment.

  • Les erreurs dans les processus fils doivent être soulevées comme des exceptions, à l'exception de SIGPIPE . Nous laissons au lecteur le soin de trouver des exceptions à la règle. SIGPIPE sur le processus final car SIGPIPE est pas On s'y attend, mais l'ignorer n'est pas dangereux.

Notez que subprocess.DEVNULL n'existe pas avant Python 3.3. Je sais que certains d'entre vous vivent toujours avec la version 2.x, vous devrez ouvrir un fichier pour /dev/null manuellement ou simplement décider que le premier processus dans le pipeline doit partager stdin avec le processus parent.

Voici le code :

import signal
import subprocess

def run_pipe(*cmds):
    """Run a pipe that chains several commands together."""
    pipe = subprocess.DEVNULL
    procs = []
    try:
        for cmd in cmds:
            proc = subprocess.Popen(cmd, stdin=pipe,
                                    stdout=subprocess.PIPE)
            procs.append(proc)
            if pipe is not subprocess.DEVNULL:
                pipe.close()
            pipe = proc.stdout
        stdout, _ = proc.communicate()
    finally:
        # Must call wait() on every process, otherwise you get
        # zombies.
        for proc in procs:
            proc.wait()
    # Fail if any command in the pipe failed, except due to SIGPIPE
    # which is expected.
    for proc in procs:
        if (proc.returncode
            and proc.returncode != -signal.SIGPIPE):
            raise subprocess.CalledProcessError(
                proc.returncode, proc.args)
    return stdout

Ici, nous pouvons le voir en action. Vous pouvez voir que le pipeline se termine correctement avec yes (qui se poursuit jusqu'à SIGPIPE ) et échoue correctement avec false (qui échoue toujours).

In [1]: run_pipe(["yes"], ["head", "-n", "1"])
Out[1]: b'y\n'

In [2]: run_pipe(["false"], ["true"])
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
<ipython-input-2-db97c6876cd7> in <module>()
----> 1 run_pipe(["false"], ["true"])

~/test.py in run_pipe(*cmds)
     22     for proc in procs:
     23         if proc.returncode and proc.returncode != -signal.SIGPIPE:
---> 24             raise subprocess.CalledProcessError(proc.returncode, proc.args)
     25     return stdout

CalledProcessError: Command '['false']' returned non-zero exit status 1

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X