Outils pour utilisateurs

Outils du site


python_subprocess

Python subprocess

Il s'agit d'exécuter des processus depuis python.
Les cas d'usage sont les suivants : faire du multiprocessing, exécuter des scripts/applications déjà optimisées dans d'autres langages (grep, …), ou des applications systèmes pour lesquelles un wrapper python n'aurait que peu d'intérêt(btrfs-send, etc.)

On va utiliser le module subprocess, intégré à la bibliothèque standard de python.

Exécuter 1 processus

La fonction de haut niveau subprocess.run() nous aidera.

import subprocess
 
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
print("STDOUT:", result.stdout)

On notera :

  • La commande est bloquante, elle attend la fin du processus pour continuer l'exécution de la suite du script python
  • Les différents arguments de la fonction sont donnés dans une liste (grosso modo, chaque espace de la ligne de commande délimite un élément de la liste)
  • On peut récupérer les entrées / sorties standard (stdin, stdout), mais également celle d'erreur (stderr)
  • On récupère un objet ici appelé result qui va nous permettre de décortiquer ce qui s'est passé

Les options

subprocess.run()
 
# capture_output : si True, capture stdout et stderr
# text = True : la sortie est un string plutôt qu'un byte
# cwd : définit le current work directory
# timeout : en secondes

Le résultat

source

On obtient un objet CompletedProcess qui contient les attributs suivants :

  • args : les arguments fournis pour exécuter le processus
  • returncode : exit status of child process, typiquement 0 si tout s'est bien passé
  • stdout : les données issues du process, en string ou byte selon le paramètre text=True (string) ou False (byte)
  • stderr : idem pour la sortie d'erreur. None s'il n'y a pas de donnée

Cas d'une commande via ssh

Admettons que l'on souhaite exécuter une commande via ssh sur une machine distante, par exemple ssh user@xxx.xxx.xxx.xxx “ls -t /tmp | head -n 1”.

Malgré la présence de guillemets “ ” sur la commande, l'argument n'en prend pas via subprocess.run([]).
En revanche, toute la commande distante est dans le même argument de subprocess.run(). ça donne :

subprocess.run(["ssh", "user@xxx.xxx.xxx.xxx", "ls -t /tmp | head -n 1"])
# on peut rajouter les options capture_output=True, text=True, ...

Popen, la fonction à la base

subprocess.Popen() est la fonction à la base de presque toutes les autres. Elle offre plus d'options, mais l'utiliser est plus complexe. Elle permet d'interagir avec les entrées et sorties de façon non bloquante.

communicate()

source Popen.communicate(input=None, timeout=None) permet d'interagir avec le processus : envoyer des données à stdin, lire des données depuis stdout et stderr (jusqu'à ce que end-of-file est atteint), attend la fin du processus et détermine le code de fin du processus (returncode attribute).

communicate() returns a tuple (stdout_data, stderr_data). The data will be strings if streams were opened in text mode; otherwise, bytes.

Note that if you want to send data to the process’s stdin, you need to create the Popen object with stdin=PIPE. Similarly, to get anything other than None in the result tuple, you need to give stdout=PIPE and/or stderr=PIPE too.

Timeout est donné en seconde. Si le processus n'est pas fini avant la fin du timeout, lève une exception TimeoutExpired.

Le returncode est disponible sur l'objet Popen directement via Popen.returncode.

Exemples avec Popen()

# -- Interagir avec les entrées / sorties -- #
 
# EXEMPLE
from subprocess import Popen, PIPE
process = Popen(["sort"], stdin=PIPE, stdout=PIPE, stderr=PIPE, text=True)
stdout, stderr = process.communicate(input="banana\napple\ncherry")
print(stdout)
# redirection des entrées et sorties
with open("input.txt", "w") as f:
    f.write("banana\napple\ncherry")
 
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
    process = Popen(["sort"], stdin=infile, stdout=outfile)
# Interagir avec les entrées / sorties
 
import subprocess
process1 = subprocess.Popen(['cat'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
process2 = subprocess.Popen(['grep', 'Hello'], stdin=process1.stdout, stdout=subprocess.PIPE)
 
# Write data to the input of the first process
process1.stdin.write(b'Hello, world!\n')
process1.stdin.close()
 
# Get the output of the second process
output = process2.communicate()[0]
 
# Print the output (ici en format byte et non string, car pas de text=True)
print(output.decode())
# -- Gérer les erreurs et exceptions -- #
import subprocess
try:
    process1 = subprocess.Popen(['echo', 'Hello, world!'], stdout=subprocess.PIPE)
    process2 = subprocess.Popen(['grep', 'Hello'], stdin=process1.stdout, stdout=subprocess.PIPE)
 
    # Get the output of the second process
    output = process2.communicate()[0]
 
    # Print the output
    print(output.decode())
 
    # Check the return code of each process
    if process1.returncode != 0:
        raise subprocess.CalledProcessError(process1.returncode, process1.args)
    if process2.returncode != 0:
        raise subprocess.CalledProcessError(process2.returncode, process2.args)
 
except subprocess.CalledProcessError as e:
    print(f"Error: {e}")

le paramètre shell=False/True

Le paramètre shell accepte 2 valeurs :

  • True : La commande est exécutée au sein d'un nouveau shell. Cela permet l'utilisation de wildcard *, et de fournir la commande comme une seule string ls -l *.txt. Risque de shell injection selon la commande, et notamment si des variables la compose (issue d'une précédente commande par ex)
  • False : La commande est exécutée directement sans invoquer un shell, les arguments sont fournis comme éléments d'une liste : [“ls”,“-l”]. Considéré comme plus secure que shell=True

Cas d'usage : le pipe, transférer la sortie d'un process à un autre

Note : le premier processus ne peut être exécuté avec subprocess.run(), on aurait une erreur. Le 1er processus doit être invoqué par subprocess.Popen(). Le 2e processus peut être invoqué par subprocess.run()

# -- Enchainer les entrées et sorties -- #
 
## EXEMPLE 1
import subprocess 
p1 = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE)
result = subprocess.run(["grep", "txt"], stdin=p1.stdout)
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
 
 
## EXEMPLE 2
import subprocess
ls_process = subprocess.Popen(["ls"], stdout=subprocess.PIPE, text=True)
grep_process = subprocess.Popen(["grep", "sample"], stdin=ls_process.stdout, stdout=subprocess.PIPE, text=True)
 
output, error = grep_process.communicate()
print(output)
print(error)

Les autres fonctions de haut niveau

source et bien sur la doc officielle de python (peut-être moins accessible/lisible).

Pour des usages facilités, le module subprocess expose d'autres fonctions de haut niveau:

  • subprocess.run() : This is the recommended method for invoking subprocesses in Python 3.5 and above. It runs a command, waits for it to finish, and then returns a CompletedProcess instance that contains information about the executed process
  • subprocess.call() : Runs a command, waits for it to finish, and then returns the return code. It's a simple way to run a command and check its return code but doesn't capture output
  • subprocess.check_call() : Similar to subprocess.call, but raises a CalledProcessError exception if the command returns a non-zero exit code
  • subprocess.check_output() : Runs a command, waits for it to finish, captures its output, and then returns that output as a byte string. It raises a CalledProcessError if the command returns a non-zero exit code
python_subprocess.txt · Dernière modification : 2024/11/02 12:46 de luc