Émulation de la 'source' de Bash en Python

91

J'ai un script qui ressemble à ceci:

export foo=/tmp/foo                                          
export bar=/tmp/bar

Chaque fois que je construis, je lance «source init_env» (où init_env est le script ci-dessus) pour configurer certaines variables.

Pour accomplir la même chose en Python, j'avais ce code en cours d'exécution,

reg = re.compile('export (?P<name>\w+)(\=(?P<value>.+))*')
for line in open(file):
    m = reg.match(line)
    if m:
        name = m.group('name')
        value = ''
        if m.group('value'):
            value = m.group('value')
        os.putenv(name, value)

Mais ensuite, quelqu'un a décidé que ce serait bien d'ajouter une ligne comme celle-ci au init_envfichier:

export PATH="/foo/bar:/bar/foo:$PATH"     

De toute évidence, mon script Python s'est effondré. Je pourrais modifier le script Python pour gérer cette ligne, mais cela se cassera plus tard lorsque quelqu'un proposera une nouvelle fonctionnalité à utiliser dans le init_envfichier.

La question est de savoir s'il existe un moyen simple d'exécuter une commande Bash et de la laisser modifier mon os.environ?

getekha
la source

Réponses:

109

Le problème avec votre approche est que vous essayez d'interpréter des scripts bash. Essayez d'abord d'interpréter l'instruction d'exportation. Ensuite, vous remarquez que les gens utilisent l'expansion variable. Plus tard, les utilisateurs mettront des conditions dans leurs fichiers ou traiteront des substitutions. À la fin, vous aurez un interpréteur de script bash complet avec un milliard de bogues. Ne fais pas ça.

Laissez Bash interpréter le fichier pour vous, puis collectez les résultats.

Vous pouvez le faire comme ceci:

#! /usr/bin/env python

import os
import pprint
import shlex
import subprocess

command = shlex.split("env -i bash -c 'source init_env && env'")
proc = subprocess.Popen(command, stdout = subprocess.PIPE)
for line in proc.stdout:
  (key, _, value) = line.partition("=")
  os.environ[key] = value
proc.communicate()

pprint.pprint(dict(os.environ))

Assurez-vous que vous gérez les erreurs au cas où bash échouerait source init_env, ou bash lui-même échouerait à s'exécuter, ou le sous-processus échouerait à exécuter bash, ou toute autre erreur.

le env -iau début de la ligne de commande crée un environnement propre. cela signifie que vous n'obtiendrez les variables d'environnement qu'à partir de init_env. si vous voulez l'environnement système hérité, omettez env -i.

Lisez la documentation sur le sous-processus pour plus de détails.

Remarque: cela ne capturera que les variables définies avec l' exportinstruction, car envn'imprime que les variables exportées.

Prendre plaisir.

Notez que la documentation Python indique que si vous souhaitez manipuler l'environnement, vous devez manipuler os.environdirectement au lieu d'utiliser os.putenv(). Je considère que c'est un bug, mais je m'éloigne du sujet.

lesmana
la source
12
Si vous vous souciez des variables non exportées et que le script est hors de votre contrôle, vous pouvez utiliser set -a pour marquer toutes les variables comme exportées. Changez simplement la commande en: ['bash', '-c', 'set -a && source init_env && env']
ahal
Notez que cela échouera sur les fonctions exportées. J'adorerais si vous pouviez mettre à jour votre réponse en montrant une analyse qui fonctionne également pour les fonctions. (par exemple, function fff () {echo "fff";}; export -f fff)
DA
2
Remarque: cela ne prend pas en charge les variables d'environnement multilignes.
BenC
2
Dans mon cas, itérer sur proc.stdout()donne des octets, donc j'obtenais un TypeErroron line.partition(). La conversion en chaîne a line.decode().partition("=")résolu le problème.
Sam F
1
C'était super utile. J'ai exécuté ['env', '-i', 'bash', '-c', 'source .bashrc && env']pour ne me donner que les variables d'environnement définies par le fichier rc
xaviersjs
32

Utilisation de cornichon:

import os, pickle
# For clarity, I moved this string out of the command
source = 'source init_env'
dump = '/usr/bin/python -c "import os,pickle;print pickle.dumps(os.environ)"'
penv = os.popen('%s && %s' %(source,dump))
env = pickle.loads(penv.read())
os.environ = env

Actualisé:

Cela utilise json, sous-processus et utilise explicitement / bin / bash (pour le support ubuntu):

import os, subprocess as sp, json
source = 'source init_env'
dump = '/usr/bin/python -c "import os, json;print json.dumps(dict(os.environ))"'
pipe = sp.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=sp.PIPE)
env = json.loads(pipe.stdout.read())
os.environ = env
Brian
la source
Celui-ci a un problème sur Ubuntu - le shell par défaut qui existe /bin/dash, qui ne connaît pas la sourcecommande. Pour l'utiliser sur Ubuntu, vous devez l'exécuter /bin/bashexplicitement, par exemple en utilisant penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(cela utilise le subprocessmodule le plus récent qui doit être importé).
Martin Pecka
22

Plutôt que d'avoir votre script Python source dans le script bash, il serait plus simple et plus élégant d'avoir une source de script wrapper init_env, puis d'exécuter votre script Python avec l'environnement modifié.

#!/bin/bash
source init_env
/run/python/script.py
John Kugelman
la source
4
Cela peut résoudre le problème dans certaines circonstances, mais pas toutes. Par exemple, j'écris un script python qui doit faire quelque chose comme l' approvisionnement du fichier (en fait, il charge des modules si vous savez de quoi je parle), et il doit charger un module différent selon certaines circonstances. Donc, cela ne résoudrait pas du tout mon problème
Davide
Cela répond à la question dans la plupart des cas, et je l'utiliserais dans la mesure du possible. J'ai eu du mal à faire ce travail dans mon IDE pour un projet donné. Une modification possible pourrait être d'exécuter le tout dans un shell avec l'environnementbash --rcfile init_env -c ./script.py
xaviersjs
6

Mise à jour de la réponse de @ lesmana pour Python 3. Notez l'utilisation de env -iqui empêche les variables d'environnement étrangères d'être définies / réinitialisées (potentiellement incorrectement étant donné le manque de gestion des variables d'environnement multilignes).

import os, subprocess
if os.path.isfile("init_env"):
    command = 'env -i sh -c "source init_env && env"'
    for line in subprocess.getoutput(command).split("\n"):
        key, value = line.split("=")
        os.environ[key]= value
Al Johri
la source
Utiliser ceci me donne "PATH: variable indéfinie" car env -i annule le chemin. Mais cela fonctionne sans env -i. Faites également attention à ce que la ligne puisse avoir plusieurs '='
Fujii
4

Exemple d'encapsulation de l'excellente réponse de @ Brian dans une fonction:

import json
import subprocess

# returns a dictionary of the environment variables resulting from sourcing a file
def env_from_sourcing(file_to_source_path, include_unexported_variables=False):
    source = '%ssource %s' % ("set -a && " if include_unexported_variables else "", file_to_source_path)
    dump = '/usr/bin/python -c "import os, json; print json.dumps(dict(os.environ))"'
    pipe = subprocess.Popen(['/bin/bash', '-c', '%s && %s' % (source, dump)], stdout=subprocess.PIPE)
    return json.loads(pipe.stdout.read())

J'utilise cette fonction utilitaire pour lire les informations d'identification aws et les fichiers docker .env avec include_unexported_variables=True.

JDiMatteo
la source