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 :

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 :

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 :

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: