Table des matières
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éresultqui 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
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 stringls -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 queshell=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
