Вызов команды "source" из подпроцесса. Popen

У меня есть .sh script, который я вызываю с помощью source the_script.sh. Регулярно звоните нормально. Тем не менее, я пытаюсь назвать это из моего python script, через 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

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

Ответ 1

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

Наиболее обычным случаем использования source является запуск оболочки script, которая изменяет среду и сохраняет эту среду в текущей оболочке. Это точно, как virtualenv работает для изменения среды python по умолчанию.

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

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

Ответ 2

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

def shell_source(script):
    """Sometime you want to emulate the action of "source" in bash,
    settings some environment variables. Here is a way to do it."""
    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)

Ответ 3

Broken Popen("source the_script.sh") эквивалентен Popen(["source the_script.sh"]) который безуспешно пытается запустить программу 'source the_script.sh'. Он не может найти его, следовательно, ошибка "No such file or directory".

Broken Popen("source the_script.sh", shell=True) завершается неудачно, потому что source - это встроенная команда bash (введите help source в bash), но оболочкой по умолчанию является /bin/sh которая ее не понимает (использует /bin/sh .) Предполагая, что в the_script.sh может быть другой bash-ism, его следует запустить с помощью bash:

foo = Popen("source the_script.sh", shell=True, executable="/bin/bash")

Как сказал @IfLoop, выполнение source в подпроцессе не очень полезно, поскольку оно не может повлиять на родительскую среду.

os.environ.update(env) -based завершаются ошибкой, если the_script.sh выполняет unset для некоторых переменных. os.environ.clear() может быть вызван для сброса среды:

#!/usr/bin/env python2
import os
from pprint import pprint
from subprocess import check_output

os.environ['a'] = 'a'*100
# POSIX: name shall not contain '=', value doesn't contain '\0'
output = check_output("source the_script.sh; env -0",   shell=True,
                      executable="/bin/bash")
# replace env
os.environ.clear() 
os.environ.update(line.partition('=')[::2] for line in output.split('\0'))
pprint(dict(os.environ)) #NOTE: only 'export'ed envvars here

Он использует env -0 и .split('\0') предложенные @unutbu

Для поддержки произвольных байтов в os.environb можно использовать модуль json (при условии, что мы используем версию Python, в которой исправлена проблема "json.dumps not parsable by json.loads"):

Чтобы избежать передачи среды через каналы, код Python можно изменить так, чтобы он вызывал себя в среде подпроцесса, например:

#!/usr/bin/env python2
import os
import sys
from pipes import quote
from pprint import pprint

if "--child" in sys.argv: # executed in the child environment
    pprint(dict(os.environ))
else:
    python, script = quote(sys.executable), quote(sys.argv[0])
    os.execl("/bin/bash", "/bin/bash", "-c",
        "source the_script.sh; %s %s --child" % (python, script))

Ответ 4

source - это встроенная оболочка bash -специфическая оболочка (и неинтерактивные оболочки часто представляют собой легкие оболочки тире вместо bash). Вместо этого просто вызовите /bin/sh:

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

Ответ 5

Вариант ответа на @xApple, поскольку иногда полезно, чтобы он мог установить оболочку script (а не файл Python) для установки переменных среды и, возможно, выполнять другие операции оболочки, а затем распространять эту среду интерпретатору Python, а не терять эту информацию при закрытии подоболочки.

Причиной вариации является то, что предположение об однострочном формате вывода из "env" не является на 100% надежным: мне просто нужно иметь дело с переменной (функция оболочки, я думаю), содержащий новую строку, которая прикручивает этот синтаксический анализ. Итак, вот немного более сложная версия, которая использует Python для форматирования словаря среды:

import subprocess
pipe = subprocess.Popen(". ./shellscript.sh; python -c 'import os; print \"newenv = %r\" % os.environ'", 
    stdout=subprocess.PIPE, shell=True)
exec(pipe.communicate()[0])
os.environ.update(newenv)

Может, есть более аккуратный способ? Это также гарантирует, что синтаксический анализ среды не будет испорчен, если кто-то помещает оператор echo в script, который будет использоваться. Конечно, здесь есть exec, поэтому будьте осторожны с недоверенным вводом... но я думаю, что это подразумевается в обсуждении того, как использовать/запускать произвольную оболочку script; -)

UPDATE: см. комментарий @unutbu в ответ @xApple для альтернативного (возможно, более приятного) способа обработки строк в env вывода.

Ответ 6

Если вы хотите применить исходную команду к некоторым другим скриптам или исполняемым файлам, тогда вы можете создать другой файл wrap script и вызвать из него команду "source" с любой необходимой логикой. В этом случае эта команда источника изменит локальный контекст, в котором он выполняется, а именно в подпроцессе, который создает подпроцесс .Popen.

Это не сработает, если вам нужно изменить контекст python, в котором работает ваша программа.

Ответ 7

Кажется, на это много ответов, не прочитали их все, чтобы они уже указали это; но при вызове команд оболочки, подобных этому, вы должны передать shell = True в вызов Popen. В противном случае вы можете вызвать Popen (shlex.split()). обязательно импортируйте shlex.

Я использую эту функцию для поиска файла и изменения текущей среды.

def set_env(env_file):
    while True:
        source_file = '/tmp/regr.source.%d'%random.randint(0, (2**32)-1)
        if not os.path.isfile(source_file): break
    with open(source_file, 'w') as src_file:
        src_file.write('#!/bin/bash\n')
        src_file.write('source %s\n'%env_file)
        src_file.write('env\n')
    os.chmod(source_file, 0755)
    p = subprocess.Popen(source_file, shell=True,
                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = p.communicate()
    setting = re.compile('^(?P<setting>[^=]*)=')
    value = re.compile('=(?P<value>.*$)')
    env_dict = {}
    for line in out.splitlines():
        if setting.search(line) and value.search(line):
            env_dict[setting.search(line).group('setting')] = value.search(line).group('value')
    for k, v in env_dict.items():
        os.environ[k] = v
    for k, v in env_dict.items():
        try:
            assert(os.getenv(k) == v)
        except AssertionError:
            raise Exception('Unable to modify environment')