.. title: Modifier et récupérer un environnement shell en Python .. slug: change-and-retrieve-bash-environment-in-python .. date: 2019-08-16 12:32:37 UTC+02:00 .. tags: python, bash, shell .. category: blog .. link: .. description: .. type: text 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. .. TEASER_END 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) : .. code-block:: python 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 : .. code-block:: python >>> 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. .. code-block:: python 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. .. code-block:: python 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 : .. code-block:: python 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 ! .. code-block:: python >>> 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 `_.