Emulando la 'fuente' de Bash en Python

91

Tengo un script que se parece a esto:

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

Cada vez que construyo, ejecuto 'source init_env' (donde init_env es el script anterior) para configurar algunas variables.

Para lograr lo mismo en Python, tenía este código ejecutándose,

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)

Pero luego alguien decidió que sería bueno agregar una línea como la siguiente al init_envarchivo:

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

Obviamente, mi script de Python se vino abajo. Podría modificar el script de Python para manejar esta línea, pero luego se romperá más adelante cuando alguien presente una nueva función para usar en el init_envarchivo.

La pregunta es si hay una manera fácil de ejecutar un comando Bash y dejar que modifique mi os.environ.

getekha
fuente

Respuestas:

109

El problema con su enfoque es que está intentando interpretar scripts de bash. Primero intente interpretar la declaración de exportación. Entonces notas que la gente está usando expansión variable. Más adelante, la gente pondrá condicionales en sus archivos o procesará sustituciones. Al final, tendrá un intérprete de guiones bash completo con un millón de errores. No hagas eso.

Deje que Bash interprete el archivo por usted y luego recopile los resultados.

Puedes hacerlo así:

#! /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))

Asegúrese de manejar los errores en caso de que bash falle source init_env, o el propio bash no se ejecute, o el subproceso no ejecute bash, o cualquier otro error.

el env -ial principio de la línea de comandos crea un ambiente limpio. eso significa que solo obtendrá las variables de entorno de init_env. si desea el entorno del sistema heredado, omita env -i.

Lea la documentación sobre el subproceso para obtener más detalles.

Nota: esto solo capturará las variables establecidas con la exportdeclaración, ya que envsolo imprime las variables exportadas.

Disfrutar.

Tenga en cuenta que la documentación de Python dice que si desea manipular el entorno, debe manipular os.environdirectamente en lugar de usar os.putenv(). Lo considero un error, pero estoy divagando.

lesmana
fuente
12
Si le preocupan las variables no exportadas y el script está fuera de su control, puede usar set -a para marcar todas las variables como exportadas. Simplemente cambie el comando a: ['bash', '-c', 'set -a && source init_env && env']
ahal
Tenga en cuenta que esto fallará en las funciones exportadas. Me encantaría si pudieras actualizar tu respuesta mostrando un análisis que también funciona para las funciones. (por ejemplo, función fff () {echo "fff";}; export -f fff)
DA
2
Nota: esto no admite variables de entorno multilínea.
BenC
2
En mi caso, interactuando sobre proc.stdout()los rendimientos de bytes, por lo que estaba recibiendo un TypeErrorsobre line.partition(). Convertir a cadena con line.decode().partition("=")resolvió el problema.
Sam F
1
Esto fue muy útil. Ejecuté ['env', '-i', 'bash', '-c', 'source .bashrc && env']entregarme sólo las variables de entorno establecidas por el archivo rc
xaviersjs
32

Usando pepinillo:

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

Actualizado:

Esto usa json, subprocess y explícitamente usa / bin / bash (para soporte de 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
fuente
Este tiene un problema en Ubuntu: el shell predeterminado que existe /bin/dash, que no conoce el sourcecomando. Para usarlo en Ubuntu, debe ejecutarlo /bin/bashexplícitamente, por ejemplo, usando penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(esto usa el subprocessmódulo más nuevo que debe importarse).
Martin Pecka
22

En lugar de tener la fuente de la secuencia de comandos de Python en la secuencia de comandos de bash, sería más simple y elegante tener una fuente de secuencia de comandos de envoltura init_envy luego ejecutar la secuencia de comandos de Python con el entorno modificado.

#!/bin/bash
source init_env
/run/python/script.py
John Kugelman
fuente
4
Puede resolver el problema en algunas circunstancias, pero no en todas. Por ejemplo, estoy escribiendo una secuencia de comandos de Python que necesita hacer algo como obtener el archivo (en realidad, carga módulos si sabe de lo que estoy hablando), y necesita cargar un módulo diferente dependiendo de algunas circunstancias. Entonces esto no resolvería mi problema en absoluto
Davide
Esto responde la pregunta en la mayoría de los casos, y lo usaría siempre que sea posible. Me costó mucho hacer que esto funcionara en mi IDE para un proyecto determinado. Una posible modificación podría ser ejecutar todo en un shell con el entornobash --rcfile init_env -c ./script.py
xaviersjs
6

Se actualizó la respuesta de @ lesmana para Python 3. Observe el uso de env -ique evita que se establezcan / restablezcan variables de entorno extrañas (potencialmente incorrectamente dada la falta de manejo de variables de env multilínea).

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
fuente
El uso de esto me da "RUTA: variable indefinida" porque env -i desarma la ruta. Pero funciona sin env -i. También tenga cuidado de que la línea pueda tener múltiples '='
Fujii
4

Ejemplo que envuelve la excelente respuesta de @ Brian en una función:

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())

Estoy usando esta función de utilidad para leer las credenciales de AWS y los archivos Docker .env con include_unexported_variables=True.

JDiMatteo
fuente