Outils pour utilisateurs

Outils du site


python_subprocess

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
python_subprocess [2024/11/01 19:54] lucpython_subprocess [2024/11/02 12:46] (Version actuelle) luc
Ligne 1: Ligne 1:
 +====== 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.
 +
 +<code python>
 +import subprocess
 +
 +result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
 +print("STDOUT:", result.stdout)
 +</code>
 +
 +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 ===
 +
 +<code python>
 +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
 +
 +</code>
 +
 +=== 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 :
 +
 +<code python>
 +subprocess.run(["ssh", "user@xxx.xxx.xxx.xxx", "ls -t /tmp | head -n 1"])
 +# on peut rajouter les options capture_output=True, text=True, ...
 +</code> 
 +
 +===== 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() ===
 +
 +<code python>
 +# -- 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)
 +
 +
 +</code>
 +
 +<code python>
 +# 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)
 +</code>
 +
 +<code python>
 +# 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())
 +</code>
 +
 +<code python>
 +# -- 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}")
 +</code>
 +
 +=== 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()''
 +
 +
 +<code python>
 +# -- 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)
 +
 +</code>
 +
 +===== 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