====== 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 ==
[[https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess|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() ===
[[https://docs.python.org/3/library/subprocess.html#subprocess.Popen.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 =====
[[https://www.golinuxcloud.com/python-subprocess/|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