Emulando 'fonte' do Bash em Python

91

Eu tenho um script parecido com este:

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

Toda vez que eu construo, eu executo 'source init_env' (onde init_env é o script acima) para configurar algumas variáveis.

Para fazer o mesmo em Python, coloquei este código em execução,

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)

Mas então alguém decidiu que seria bom adicionar uma linha como a seguinte ao init_envarquivo:

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

Obviamente, meu script Python se desfez. Eu poderia modificar o script Python para lidar com essa linha, mas ele apenas será interrompido mais tarde, quando alguém surgir com um novo recurso para usar no init_envarquivo.

A questão é se existe uma maneira fácil de executar um comando Bash e deixá-lo modificar o meu os.environ?

getekha
fonte

Respostas:

109

O problema com sua abordagem é que você está tentando interpretar scripts bash. Primeiro, você apenas tenta interpretar a instrução de exportação. Então você percebe que as pessoas estão usando expansão variável. Posteriormente, as pessoas colocarão condicionais em seus arquivos ou substituições de processos. No final, você terá um interpretador de script bash completo com zilhões de bugs. Não faça isso.

Deixe o Bash interpretar o arquivo para você e coletar os resultados.

Você pode fazer assim:

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

Certifique-se de lidar com os erros caso o bash falhe source init_env, ou o próprio bash falhe ao executar, ou o subprocesso falhe ao executar o bash, ou quaisquer outros erros.

o env -ino início da linha de comando cria um ambiente limpo. isso significa que você só obterá as variáveis ​​de ambiente de init_env. se desejar o ambiente de sistema herdado, omita env -i.

Leia a documentação sobre o subprocesso para mais detalhes.

Nota: isso irá capturar apenas variáveis ​​definidas com a exportinstrução, pois envimprime apenas variáveis ​​exportadas.

Aproveitar.

Observe que a documentação do Python diz que se você deseja manipular o ambiente, deve manipular os.environdiretamente em vez de usar os.putenv(). Eu considero isso um bug, mas estou divagando.

Lesmana
fonte
12
Se você se preocupa com variáveis ​​não exportadas e o script está fora de seu controle, você pode usar set -a para marcar todas as variáveis ​​como exportadas. Basta alterar o comando para: ['bash', '-c', 'set -a && source init_env && env']
ahal
Observe que isso falhará nas funções exportadas. Eu adoraria se você pudesse atualizar sua resposta mostrando uma análise que funciona para funções também. (por exemplo, função fff () {echo "fff";}; export -f fff)
DA
2
Observação: isso não oferece suporte a variáveis ​​de ambiente de várias linhas.
BenC
2
No meu caso, a iteração em proc.stdout()bytes de rendimento, portanto, eu estava ficando TypeErrorligado line.partition(). A conversão para string com line.decode().partition("=")resolveu o problema.
Sam F
1
Isso foi muito útil. I executada ['env', '-i', 'bash', '-c', 'source .bashrc && env']para me dar apenas as variáveis de ambiente definidas pelo arquivo rc
xaviersjs
32

Usando picles:

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

Atualizada:

Isso usa json, subprocess, e usa explicitamente / bin / bash (para suporte 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
fonte
Este tem um problema no Ubuntu - o shell padrão que existe /bin/dash, que não conhece o sourcecomando. Para usá-lo no Ubuntu, você deve executar /bin/bashexplicitamente, por exemplo, usando penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(usa o subprocessmódulo mais recente que deve ser importado).
Martin Pecka
22

Em vez de ter sua fonte de script Python como o script bash, seria mais simples e mais elegante ter uma fonte de script wrapper init_enve então executar seu script Python com o ambiente modificado.

#!/bin/bash
source init_env
/run/python/script.py
John Kugelman
fonte
4
Pode resolver o problema em algumas circunstâncias, mas não em todas. Por exemplo, estou escrevendo um script python que precisa fazer algo como a origem do arquivo (na verdade, ele carrega módulos se você sabe do que estou falando) e precisa carregar um módulo diferente dependendo de algumas circunstâncias. Portanto, isso não resolveria o meu problema de forma alguma
Davide
Isso responde à pergunta na maioria dos casos e eu o usaria sempre que possível. Tive dificuldade em fazer esse trabalho em meu IDE para um determinado projeto. Uma possível modificação pode ser executar tudo em um shell com o ambientebash --rcfile init_env -c ./script.py
xaviersjs
6

Atualizado a resposta de @lesmana para Python 3. Observe o uso de env -ique evita que variáveis ​​de ambiente estranhas sejam definidas / redefinidas (potencialmente incorretamente devido à falta de manipulação de variáveis ​​env multilinha).

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
fonte
Usar isso me dá "PATH: variável indefinida" porque env -i cancela a definição do caminho. Mas funciona sem o env -i. Também tome cuidado para que a linha possa ter vários '='
Fujii
4

Exemplo de envolvimento da excelente resposta de Brian em uma função:

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

Estou usando esta função de utilitário para ler credenciais aws e arquivos .env do docker com include_unexported_variables=True.

JDiMatteo
fonte