É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?


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.


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

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

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

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.