Modifier et récupérer un environnement shell en Python
Python est un langage très versatile et qui s'interface généralement bien avec d'autres langages, et donc il est également assez facile d'interagir avec le shell de son système. Le problème est que lorsqu'on communique avec son système, c'est un sous-shell qui est lancé, ce qui signifie que toute modification de l'environnement est perdue et qu'on ne peut pas lancer plusieurs commandes shell indépendamment qui peuvent être liées (via l'environnement). Ce problème a été à l'origine d'une question sur Stackoverflow, à laquelle j'ai fini par répondre moi-même.
Le problème
Un exemple minimal de ce qu'on pourrait souhaiter faire et le suivant (on définit une variable, et on tente ensuite de l'afficher) :
from subprocess import getstatusoutput as cmd stat, out = cmd("export TEST=1") stat, out = cmd("echo $TEST")
où on espère que le retour du deuxième appel renvoie 1. Sauf que l'expérience montre que $TEST est vide :
>>> print(out) (0, "")
ce qui s'explique par le fait que chacune des commandes s'est exécutée dans un sous-shell, et que l'environnement n'a pas été passé de l'un à l'autre.
La solution
L'idée, assez simple, est de récupérer l'environnement en... l'affichant. En parsant ensuite cette sortie, il est possible de récupérer l'ensemble des variables de l'environnement et de les communiquer au sous-shell suivant. La solution tient alors en deux fonctions :
- une fonction launch qui va être un wrapper d'une commande de subprocess pour exécuter des commandes shell, et qui va nous permettre de récupérer un environnement en ajoutant un printenv à la fin de la commande exécutée ;
- une fonction get_env qui va parser le retour de la fonction précédente et stocker les variables récupérées dans un dictionnaire.
import os import subprocess as sp def launch(cmd_, env=os.environ, get_env=False): if get_env: cmd_ += " && printenv" load = sp.Popen(cmd_, shell=True, stdout=sp.PIPE, stderr=sp.PIPE, env=env) out = load.communicate() err = load.returncode return(err, out)
On voit que launch prend trois arguments :
- la commande shell qu'on veut exécuter ;
- un environnement, qui n'est rien d'autre qu'un dictionnaire de variables ;
- un booléen spécifiant si l'on souhaite récupérer — ou non — l'environnement.
def get_env(out, encoding='utf-8'): lout = str(out[0], encoding).split('\n') new_env = {} for line in lout: if len(line.split('=')) <= 1: pass else: k = line.split("=")[0] v = "=".join(line.split("=")[1:]) new_env[k] = v return new_env
La commande get_env prend simplement en argument la sortie de la fonction précédente, et un encodage (pour éviter les mauvaises surprises).
Le tout s'utilise au final de la manière suivante :
err, out = launch("export TEST=1", get_env=True) if not err: new_env = get_env(out) err, out = launch("echo $TEST", env=new_env)
Et on voit que désormais $TEST est connu !
>>> print(str(out[0], encoding='utf-8')) 1
On peut écrire des fonctions un peu plus complexes, entre autres pour traiter des cas où l'environnement contient des fonctions. Une version plus complète et plus complexe est disponible sur mon GitHub.