Skip to content

Changer l'environnement bash avec python

Il est possible de changer l'environnement bash en "sourçant" un script bash. Mais comment ça se passe si on veut le faire avec un script python ?

Petit rappel

Sous unix, chaque processus s'exécute dans un contexte particulier. Une des 'caractéristiques' de ce contexte est l'environnement d'exécution représenté par les variables d'environnement. Chaque processus (excepté le premier, init) est lancé par un autre processus (le processus père) et hérite d'une copie de l'environnement de ce dernier. Comme le processus fils accède à une copie de l'environnement du processus père, il ne peut pas le modifier. Ainsi, sous bash, si vous exécutez un programme (ou un script), à la sortie de ce script, l'environnement du processus père (votre terminal ici) n'est pas modifié. Par contre, si vous interprétez le script dans le processus courant à l'aide de l'instruction source, l'environnement du processus est modifié (notez bien qu'il n'y a pas de processus père ou fils, car il n'y a qu'un seul processus) Prenez ce script simple que nous appellerons essai.sh (oui, je sais, ce n'est pas très imaginatif mais bon, ce n'est pas le but de ce billet) :

echo "variable TRUC : $TRUC"
echo "modification de la variable d'environnement TRUC"
export TRUC="machin"
echo "variable TRUC après modification : $TRUC"

Et dans un terminal, exécutez c'est quelques commandes :

$ export TRUC="bidule"
$ echo $TRUC
bidule
$ ./essai.sh
variable TRUC : bidule
modification de la variable d'environnement TRUC
variable TRUC après modification : machin
$ echo $TRUC
bidule
$ source essais.sh
variable TRUC : bidule
modification de la variable d'environnement TRUC
variable TRUC après modification : machin
$ echo $TRUC
machin

On voit bien que la variable d'environnement TRUC n'as pas été modifiée dans l'environnement du processus père lorsqu'on exécute le script, mais que, quand on le "source", l'environnement est bien modifié. Tout ceci marche bien quand on reste dans bash. Par contre, cela ne fonctionne pas avec python puisque que, bash n'interprétant que des scripts bash et pas des scripts python, on ne peut les "sourcer".

L'astuce

Si on ne peut interpréter un script python avec bash il faut le faire avec python. Une fois le script interprété, il nous reste plus qu'à traduire l'environnement python en bash. Comment ? En prenant toutes les variables d'environnement et pour chacune d'entre elles, créer et exécuter une instruction bash correspondante. Pour cela nous allons avoir :

  • Un script python qui change l'environnement. C'est le script utilisateur qui est l'équivalent python de essai.sh
  • Un script python (pylauncher.py) qui va lancer le script utilisateur et qui, en sortie, va donner les instructions bash correspondantes à l'environnement modifié par le script utilisateur.
  • Un script bash (pysource.sh) qui va lancer pylauncher.py et évaluer les instructions bash.
  • Un script bash (ou notre terminal) qui lance le tout et qui veut voir son environnement modifié.

Le script utilisateur : C'est le script python qui modifie l'environnement, c'est à vous de le créer en fonction de vos besoins. Ici nous prendrons un script qui fait la même chose que essai.sh :

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import os
print "variable TRUC :",os.getenv("TRUC")
print "modification de la variable d'environnement TRUC"
os.environ["TRUC"]="machin"
print "variable TRUC après modification :",os.getenv("TRUC")

pylauncher.py

#!/usr/bin/env python
#-*- coding:utf-8 -*-

# Pour éviter les conflits de nom, on sauvegarde l'espace de nom global avant qu'il ne soit "pollué" par nos variables.
__globals__ = dict(globals())

# dump_env s'occupe de traduire l'environnement python en instructions bash.
def dump_env(dumpOutput):
    import os
    for key in os.environ:
        dumpOutput.write('export %(key)s="%(value)s";\n'%{'key':key,'value':os.environ[key]})

if __name__=="__main__":
    import sys
    import os

    # On redéfinie la sortie standard. Ainsi le script utilisateur fera ses "print" sur le descripteur de fichier 3 et on imprimera l'environnement sur le descripteur de fichier 1
    dumpOutput=sys.stdout
    sys.stdout=os.fdopen(3,'w')

    # On modifie quelques variables pour que le script utilisateur n'est pas conscience de pylauncher.py
    __globals__['__file__'] = sys.argv[1]
    sys.argv=sys.argv[1:]

    try:
        # On exécute le script utilisateur.
        # On redéfinie les espaces de nom globaux et locaux pour les mêmes raisons que le commentaire précédent.
        # On n'utilise pas os.system ou subprocess, car justement, on ne veut pas d'un processus fils. execfile est l'équivalent d'un source, mais à la mode python.
        execfile(sys.argv[0], __globals__, dict())
        # On imprime l'environnement.
        dump_env(dumpOutput)
    except IOError:
        print(sys.exc_info()[1])

Pensez à rendre pylauncher.py exécutable puisqu'il sera lancé par sourcepy.sh

Le script sourcepy.sh

#!/bin/bash

line="./pylauncher.py $@"

# On initialise le descripteur de fichier 3.
exec 3>&1

# À ce moment on a trois descripteurs de fichier ouverts :
# - le numéro 1 qui pointe vers la sortie standard. (là où sont imprimées les instructions bash)
# - le numéro 2 qui pointe vers la sortie d'erreur.
# - le numéro 3 qui pointe vers la sortie standard. (là où sont imprimées les sorties standard du script utilisateur)

# On lance pylauncher.py qui va lancer le script utilisateur et on évalue les instructions bash pour retrouver l'environnement modifier par le script utilisateur
eval `$line`

# On ferme le descripteur de fichier 3.;;
exec 3>&-

Notez qu'il n'est pas nécessaire de créer un fichier sourcepy.sh, vous pouvez en faire une fonction que vous mettrez dans votre .bashrc

On lance le tout :

Dans un terminal :

$ ls
pylauncher.py
essai.py
$ export TRUC="bidule"
$ source sourcepy.sh essai.py # ou sourcepy essai.py si on a fait une fonction sourcepy
variable TRUC : bidule
modification de la variable d'environnement TRUC
variable TRUC après modification : machin
$ echo $TRUC
machin

Et voilà !