0

Вызов команды "source" из subprocess.Popen

56

У меня есть скрипт .sh, который я запускаю с помощью команды source the_script.sh. Всё работает нормально, но теперь я пытаюсь вызвать его из своего Python-скрипта с использованием subprocess.Popen.

Когда я вызываю его через Popen, я получаю следующие ошибки в двух различных сценариях:

foo = subprocess.Popen("source the_script.sh")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/subprocess.py", line 672, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1213, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory


>>> foo = subprocess.Popen("source the_script.sh", shell=True)
>>> /bin/sh: source: not found

Что происходит? Почему я не могу вызвать "source" из Popen, когда у меня это получается сделать в командной строке?

5 ответ(ов)

0

source — это не исполняемая команда, а встроенная команда оболочки.

Самый распространенный случай использования source — это выполнение скрипта оболочки, который изменяет окружение, чтобы сохранить это окружение в текущей оболочке. Именно так работает virtualenv, изменяя стандартное окружение Python.

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

В Python есть аналогичная команда execfile, которая выполняет указанный файл с использованием текущего глобального пространства имен Python (или другого, если вы его укажете). Эту команду можно использовать аналогично команде source в bash.

0

Вы можете просто выполнить команду в подсохранении и использовать полученные результаты для обновления текущей среды.

def shell_source(script):
    """Иногда вам нужно эмулировать действие "source" в bash,
    устанавливая некоторые переменные окружения. Вот как это можно сделать."""
    import subprocess, os
    pipe = subprocess.Popen(". %s; env" % script, stdout=subprocess.PIPE, shell=True)
    output = pipe.communicate()[0]
    env = dict((line.split("=", 1) for line in output.splitlines()))
    os.environ.update(env)

Этот код выполняет скрипт в оболочке и собирает все переменные окружения, которые были установлены в результате выполнения скрипта. Затем он обновляет переменные окружения текущего процесса. Обратите внимание, что такой подход работает только для текущего процесса и не затрагивает родительский процесс.

0

source — это встроенная команда, специфичная для bash, и в неинтерактивных оболочках обычно используются более легковесные оболочки, такие как dash. Вместо этого просто вызовите /bin/sh:

foo = subprocess.Popen(["/bin/sh", "the_script.sh"])

Таким образом, вы сможете выполнить скрипт в совместимой оболочке.

0

Обновление: 2019

Иногда вам нужно эмулировать действие команды "source" в Bash для установки некоторых переменных окружения. Вот один из способов сделать это.

def shell_source(str_script, lst_filter):
    # Обходная мера для разрешения использования переменных с переносами строк
    # Пример: MY_VAR='foo\n'
    # env -i создает чистую оболочку
    # bash -c выполняет команду в bash
    # set -a опционально, если вы хотите экспортировать как переменные оболочки, так и переменные среды
    # env -0 разделяет переменные нулевым символом вместо символа новой строки
    command = shlex.split(f"env -i bash -c 'set -a && source {str_script} && env -0'")

    pipe = subprocess.Popen(command, stdout=subprocess.PIPE)
    # pipe теперь выводит байты, поэтому преобразуем в строку utf перед разбором
    output = pipe.communicate()[0].decode('utf-8')
    # У меня всегда была последняя пустая строка, поэтому я удаляю ее. Удалите эту строку, если у вас это не происходит.
    output = output[:-1]

    pre_filter_env = {}
    # Разделяем, используя нулевой символ
    for line in output.split('\x00'):
        line = line.split('=', 1)
        pre_filter_env[line[0]] = line[1]

    post_filter_env = {}
    for str_var in lst_filter:
        post_filter_env[str_var] = pre_filter_env[str_var]

    os.environ.update(post_filter_env)

Этот код запускает Bash в чистой среде и использует source для выполнения указанного скрипта, затем извлекает переменные окружения и обновляет текущую среду Python. Подход отлично подходит для тех случаев, когда необходимо интерактивно управлять переменными окружения из внешних скриптов.

0

Используя ответы из этой темы, я разработал решение, которое полностью удовлетворяет мои потребности.

  • нет необходимости фильтровать переменные окружения
  • позволяет использовать переменные с символами новой строки
def shell_source(script):
    """
    Иногда вам нужно эмулировать действие "source" в bash,
    устанавливая некоторые переменные окружения. Вот способ сделать это.
    """
    
    pipe = subprocess.Popen(". %s && env -0" % script, stdout=subprocess.PIPE, shell=True)
    output = pipe.communicate()[0].decode('utf-8')
    output = output[:-1]  # исправление для индекса вне диапазона в 'env[ line[0] ] = line[1]'

    env = {}
    # делим по нулевому символу
    for line in output.split('\x00'):
        line = line.split('=', 1)
        env[line[0]] = line[1]

    os.environ.update(env)

С этим решением я смог запускать команды с теми же переменными окружения без проблем:

def runCommand(command):
    """
    Печатает, а затем выполняет команду оболочки.
    """
    print(f'> выполняется: {command}')
    stream = subprocess.Popen(command, shell=True, env=os.environ)
    (result_data, result_error) = stream.communicate()
    print(f'{result_data}, {result_error}')

Надеюсь, это поможет кому-то, кто оказался в такой же ситуации, как я.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь