Эмуляция "исходного кода" Bash в Python

92

У меня есть сценарий, который выглядит примерно так:

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

Каждый раз, когда я создаю, я запускаю 'source init_env' (где init_env - это сценарий выше), чтобы установить некоторые переменные.

Чтобы сделать то же самое в Python, у меня был запущен этот код,

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)

Но потом кто-то решил, что было бы неплохо добавить в init_envфайл такую строку:

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

Очевидно, мой скрипт Python развалился. Я мог бы изменить скрипт Python для обработки этой строки, но потом он просто сломается, когда кто-нибудь предложит новую функцию для использования в init_envфайле.

Вопрос в том, есть ли простой способ запустить команду Bash и позволить ей изменить мою os.environ?

гетеха
источник

Ответы:

109

Проблема с вашим подходом в том, что вы пытаетесь интерпретировать сценарии bash. Сначала вы просто пытаетесь интерпретировать оператор экспорта. Затем вы замечаете, что люди используют расширение переменных. Позже люди будут помещать в свои файлы условные обозначения или обрабатывать замены. В конце концов, у вас будет полноценный интерпретатор сценариев bash с кучей ошибок. Не делай этого.

Пусть Bash интерпретирует файл за вас, а затем соберет результаты.

Сделать это можно так:

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

Убедитесь, что вы обрабатываете ошибки в случае, если bash не работает source init_env, или сам bash не может выполняться, или подпроцесс не может выполнить bash, или любые другие ошибки.

env -iв начале командной строки создает чистую окружающую среду. это означает, что вы получите только переменные среды из init_env. если вам нужна унаследованная системная среда, опустите env -i.

Прочтите документацию по подпроцессу для получения более подробной информации.

Примечание: это будет захватывать только переменные, установленные с помощью exportоператора, поскольку envпечатает только экспортированные переменные.

Наслаждаться.

Обратите внимание, что в документации Python говорится, что если вы хотите манипулировать средой, вы должны манипулировать os.environнапрямую, а не использовать os.putenv(). Я считаю это ошибкой, но я отвлекся.

лесмана
источник
12
Если вы заботитесь о неэкспортируемых переменных и сценарий находится вне вашего контроля, вы можете использовать set -a, чтобы пометить все переменные как экспортированные. Просто измените команду на: ['bash', '-c', 'set -a && source init_env && env']
ахал
Обратите внимание, что это не удастся для экспортируемых функций. Мне бы очень хотелось, если бы вы могли обновить свой ответ, показывая синтаксический анализ, который также работает для функций. (например, function fff () {echo "fff";}; export -f fff)
DA
2
Примечание: это не поддерживает многострочные переменные среды.
BenC
2
В моем случае, итерация по proc.stdout()урожайности байт, таким образом , я получаю TypeErrorна line.partition(). Преобразование в строку с line.decode().partition("=")решением проблемы.
Sam F
1
Это было очень полезно. Я выполнил, ['env', '-i', 'bash', '-c', 'source .bashrc && env']чтобы предоставить себе только переменные среды, установленные файлом rc
xaviersjs
32

Использование рассола:

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

Обновлено:

Это использует json, subprocess и явно использует / bin / bash (для поддержки 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
Брайан
источник
У этого есть проблема в Ubuntu - есть оболочка по умолчанию /bin/dash, которая не знает sourceкоманды. Чтобы использовать его в Ubuntu, вы должны запустить его /bin/bashявно, например, используя penv = subprocess.Popen(['/bin/bash', '-c', '%s && %s' %(source,dump)], stdout=subprocess.PIPE).stdout(здесь используется более новый subprocessмодуль, который необходимо импортировать).
Мартин Пецка
22

Вместо того, чтобы иметь в качестве источника сценария Python сценарий bash, было бы проще и элегантнее иметь источник сценария-оболочки, init_envа затем запускать сценарий Python в измененной среде.

#!/bin/bash
source init_env
/run/python/script.py
Джон Кугельман
источник
5
В некоторых случаях это может решить проблему, но не во всех. Например, я пишу сценарий python, который должен делать что-то вроде поиска файла (на самом деле он загружает модули, если вы знаете, о чем я говорю), и ему необходимо загрузить другой модуль в зависимости от некоторых обстоятельств. Так что это вообще не решит мою проблему,
Давиде
В большинстве случаев это действительно ответ на вопрос, и я бы использовал его везде, где возможно. Мне было сложно выполнить эту работу в моей среде IDE для конкретного проекта. Одна из возможных модификаций может заключаться в запуске всего этого в оболочке с окружениемbash --rcfile init_env -c ./script.py
xaviersjs
6

Обновлен ответ @ lesmana для Python 3. Обратите внимание, что использование этого параметра env -iпредотвращает установку / сброс посторонних переменных среды (возможно, неправильно, учитывая отсутствие обработки многострочных переменных env).

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
источник
Использование этого дает мне «ПУТЬ: неопределенная переменная», потому что env -i отменяет путь. Но он работает без env -i. Также будьте осторожны, чтобы в строке могло быть несколько знаков '='
Fujii
5

Пример упаковки отличного ответа @Brian в функцию:

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

Я использую эту служебную функцию для чтения учетных данных aws и файлов docker .env с помощью include_unexported_variables=True.

JDiMatteo
источник