subprocess : lancer une commande depuis Python
Lancer une commande avec subprocess.run et une liste d'arguments, lire son code de retour et sa sortie, transformer un échec en exception avec check=True, borner l'attente avec timeout, et adapter la commande au système avec platform.system et shutil.which.
Mis à jour le
À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.
Pourquoi on voit ça
Python ne sait pas tout faire seul. Pour savoir si un serveur répond, le plus simple est de lancer ping. Pour lire l’état d’un dépôt, c’est git. Pour l’espace disque, df. Ces commandes existent déjà : ton outil doit les lancer, puis lire ce qu’elles répondent.
C’est le rôle de subprocess.run. La fonction est simple, mais chaque argument change ce que tu récupères, et une mauvaise habitude (shell=True) ouvre une vraie faille de sécurité. Attends-toi à plusieurs extraits de ce genre au contrôle.
subprocess.run et la liste d’arguments
La commande se donne sous forme de liste : le programme d’abord, puis un élément par argument.
import subprocess
subprocess.run(["ping", "-c", "1", "web-1"])
Pas de chaîne unique, pas d’espaces à gérer : ["ping", "-c", "1", "web-1"], et non "ping -c 1 web-1".
Pourquoi pas shell=True
Avec shell=True, tu donnes une seule chaîne, et c’est un shell qui la lit. Or un shell interprète des caractères spéciaux : ;, |, &&, $, *. Si un morceau de la chaîne vient de l’extérieur (un inventaire, un argument, une API), celui qui l’écrit peut y glisser une commande.
host = "web-1; echo PIRATE"
subprocess.run(["echo", "ping", host])
subprocess.run(f"echo ping {host}", shell=True)
ping web-1; echo PIRATE
ping web-1
PIRATE
Première ligne : la liste transmet host comme un seul argument, tel quel. Le point-virgule n’est qu’un caractère. Lignes suivantes : le shell a coupé au point-virgule et exécuté une deuxième commande. Remplace echo PIRATE par rm -rf / et tu vois le problème. On appelle ça une injection de commande.
Règle : toujours une liste, jamais shell=True.
Le code de retour : returncode
run attend la fin de la commande et renvoie un objet résultat. Son champ returncode est le code de sortie de la commande : 0 = tout s’est bien passé, autre chose = échec. C’est le $? du shell.
Pour s’entraîner, deux commandes minuscules : true réussit toujours (code 0), false échoue toujours (code 1).
r = subprocess.run(["true"])
print(r.returncode)
r = subprocess.run(["false"])
print(r.returncode)
0
1
Le point à retenir : un échec de la commande ne lève pas d’exception. run te donne le code, et c’est à toi de le tester.
r = subprocess.run(["ping", "-c", "1", "web-1"])
if r.returncode == 0:
print("web-1 répond")
else:
print("web-1 ne répond pas")
Récupérer la sortie : capture_output et text
Par défaut, ce que la commande écrit part directement dans le terminal. Python ne le voit pas : r.stdout vaut None.
Pour le récupérer, il faut deux arguments :
| Arguments | r.stdout vaut |
|---|---|
| aucun | None (la sortie s’affiche dans le terminal) |
capture_output=True |
des octets : b'ok\n' |
capture_output=True, text=True |
du texte : 'ok\n' |
r = subprocess.run(["echo", "web-1"], capture_output=True, text=True)
print(repr(r.stdout))
print(r.stdout.strip())
'web-1\n'
web-1
La sortie se termine par un saut de ligne : pense à strip() avant de comparer ou d’afficher.
Une commande a deux canaux de sortie, et tu récupères les deux :
r.stdout: le résultat normal ;r.stderr: les messages d’erreur.
Quand ls vise un dossier qui n’existe pas, r.stdout est une chaîne vide, et le message « No such file or directory » est dans r.stderr. Le returncode est alors différent de 0.
check=True : l’échec devient une exception
Tester returncode après chaque commande est vite lourd. Avec check=True, run lève CalledProcessError dès que le code de retour n’est pas 0.
try:
subprocess.run(["false"], check=True)
print("ok")
except subprocess.CalledProcessError as e:
print("échec", e.returncode)
échec 1
L’exception porte le code dans e.returncode. Si la commande réussit, rien n’est levé et le code continue normalement.
Sans check |
Avec check=True |
|
|---|---|---|
| Commande réussie (code 0) | returncode vaut 0 |
rien de spécial |
| Commande en échec | returncode vaut le code, pas d’exception |
CalledProcessError |
timeout : ne pas attendre pour toujours
Un ping vers une machine éteinte, une commande qui attend une saisie : sans limite, ton outil reste bloqué. timeout= donne un délai maximal en secondes. Passé ce délai, run arrête la commande et lève TimeoutExpired.
try:
subprocess.run(["sleep", "5"], timeout=1)
print("terminé")
except subprocess.TimeoutExpired:
print("trop long")
trop long
Le message arrive au bout d’une seconde, pas de cinq. Ne confonds pas les deux exceptions : CalledProcessError = la commande a fini, mais mal ; TimeoutExpired = la commande n’a pas fini à temps.
S’adapter à la machine : platform.system et shutil.which
Ton outil tourne sur ton poste et sur des serveurs. Les commandes ne sont pas les mêmes partout.
platform.system() renvoie le nom du système :
| Système | Valeur renvoyée |
|---|---|
| Linux | "Linux" |
| macOS | "Darwin" |
| Windows | "Windows" |
Exemple classique : ping envoie un seul paquet avec -c 1 sous Linux et macOS, mais avec -n 1 sous Windows.
import platform
if platform.system() == "Windows":
option = "-n"
else:
option = "-c"
subprocess.run(["ping", option, "1", "web-1"])
shutil.which("git") cherche la commande comme le ferait le shell. Il renvoie son chemin (par exemple /usr/bin/git), ou None si elle n’existe pas. Il ne lève jamais d’exception.
import shutil
if shutil.which("git") is None:
print("git n'est pas installé")
Pourquoi vérifier avant ? Parce que lancer une commande absente avec subprocess.run lève FileNotFoundError. Avec which, tu peux afficher un message clair à la place d’un traceback.
À retenir
subprocess.run(["ping", "-c", "1", "web-1"]): toujours une liste, un élément par argument. Jamaisshell=True(injection de commande).r.returncode: 0 = succès. Sanscheck, un échec ne lève rien.capture_output=True, text=Trueremplitr.stdoutetr.stderravec du texte. Sans capture, ils valentNone.check=TruelèveCalledProcessErrorsi le code n’est pas 0.timeout=5lèveTimeoutExpiredsi la commande dépasse 5 secondes.platform.system()donne"Linux","Darwin"ou"Windows";shutil.which("git")donne un chemin, ouNonesi la commande n’existe pas.
Entraînement
12 questions, dont 4 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.
Ta note
Cartes · 8
Réviser les cartes ici
Pourquoi