Емуляція "джерела" Bash у Python


91

У мене є сценарій, який виглядає приблизно так:

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-скриптів із мільйонами помилок. Не роби цього.

Нехай Баш інтерпретує файл для вас, а потім збирає результати.

Ви можете зробити це так:

#! /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']
ahal

Зверніть увагу, що це не вдасться для експортованих функцій. Я б дуже хотів, якщо б ви могли оновити свою відповідь, показавши аналіз, який працює і для функцій. (наприклад, функція 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, підпроцес та явно використовує / 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

4
Це може вирішити проблему за певних обставин, але не у всіх. Наприклад, я пишу сценарій 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

Використовуючи це, я отримую "PATH: undefined variable", оскільки env -i скасовує шлях. Але це працює без env -i. Також будьте обережні, щоб у рядку було кратно '='
Фудзі

4

Приклад загортання чудової відповіді Брайана у функцію:

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 та докерування файлів .env за допомогою include_unexported_variables=True.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.