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é
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
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 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 processsubprocess.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 outputsubprocess.check_call()
: Similar to subprocess.call, but raises a CalledProcessError exception if the command returns a non-zero exit codesubprocess.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