使用subprocess.Popen调用“source”命令

56

我有一个 .sh 脚本,我通过 source the_script.sh 来调用它。正常情况下这样调用是没问题的。但是,我试图通过 subprocess.Popen 在我的 python 脚本中调用它。

通过 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调用"source",但在Python外部可以呢?


pypa/hatchShellManager(许可证MIT)是否适合您? - tony
如果Popen类似于POSIX C中的popen函数,那么它的主要参数是要执行的shell脚本。您可以在该shell脚本中执行解决问题所需的任何操作,例如. /path/to/env.sh ; binary。先加载环境脚本,然后用分号隔开,再运行二进制文件。如果环境脚本可能会失败,或者可能找不到等情况,我们可以使用&&代替分号,以避免在这种情况下尝试运行程序。 - Kaz
9个回答

49

source不是一个可执行的命令,而是一个 shell 内置命令。

通常使用 source 命令是为了运行修改环境变量的脚本,并将这些修改后的环境变量保留在当前 shell 中。这正是 virtualenv 用来修改默认 python 环境的方法。

在子进程中创建并使用 source 命令可能没有任何有用的效果,它不会修改父进程的环境变量,也不会产生任何使用 sourced 脚本的副作用。

Python 有一个类似的命令 execfile,它使用当前的 python 全局命名空间(或者你提供的其他命名空间)运行指定的文件,你可以类比 bash 命令 source 在类似的方式中使用它。


1
请注意,虽然execfile是Python程序中的确切类比,但在Python程序中,几乎总是使用import来代替您通常在shell脚本中使用的source - Rosh Oxymoron
有趣。所以即使我按照phihag建议的做法,环境变量的更改也不会被实际保存下来? - coffee
好的,它们将停留在bash子进程中,但这对您有什么好处取决于the_script.sh实际执行的操作。一个旨在通过“source”调用的脚本在子进程中使用的可能性不大。 - SingleNegationElimination
1
“source: not found” - 问题在于 /bin/sh 不支持,但 /bin/bash 支持 - jfs
为什么你不能只是从管道中读取并将环境变量复制到父进程中的一个函数中呢? - frei
2
注意:在Python3中,execfile()已被替换为exec() - Josh Correia

33

您可以在子shell中运行命令,并使用结果更新当前环境。

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)

2
应该给出信用:这来自http://pythonwise.blogspot.fr/2010/04/sourcing-shell-script.html(尽管xApple == Miki?)但是需要注意的一点是:通常脚本参数需要是显式路径,即“myenv.sh”通常不起作用,但“./myenv.sh”会。这是因为在具有sh shell严格实现的系统上(例如Debian / Ubuntu),sourcing内置(.)的行为。 - andybuckley
@andybuckley评论得好。使用"./myenv.sh"替代"myenv.sh"。 - diabloneo
3
如果环境变量的值包含换行符,此函数可能会引发 ValueError。要 修复,请使用 env -0output.split('\x00') - unutbu
1
@unutbu:我已经使用了您的建议来支持环境变量值中除'\0'以外的任意字节 - jfs
1
我尝试了 shell_source("./myscript.sh") ,但是它给我报错 ./myscript.sh: 88: Syntax error: end of file unexpected (expecting "then") Traceback (most recent call last): File "readflags_pkg_0V01.py", line 41, in shell_source("./myscript.sh") File "readflags_pkg_0V01.py", line 38, in shell_source env = dict((line.split("=", 1) for line in output.split('\x00'))) ValueError: dictionary update sequence element #0 has length 1; 2 is required 我做得对吗?shell_script()中的参数是文件对象还是直接是文件名? - Sami
@Sami:你的"./myscript.sh"脚本可能有'echo'或其他标准输出,这些输出不能被shell解析为命令。你可以将这些行的STDOUT重定向到STDERR。我没有通用的解决方案。 - josefwells

25

破损的 Popen("source the_script.sh") 等同于 Popen(["source the_script.sh"]),试图无法成功启动程序 'source the_script.sh',因为找不到它,所以会出现 "No such file or directory" 错误。

破损的Popen("source the_script.sh", shell=True) 失败了,因为 source 是一个 bash 的内置命令(在 bash 中键入 help source),但默认的 shell 是 /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)的方法会因为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

它使用了@unutbu建议的env -0.split('\0')

为了支持os.environb中的任意字节,可以使用json模块(假设我们使用修复了"json.dumps not parsable by json.loads" issue问题的Python版本):

为了避免通过管道传递环境变量,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))

1
为了在Python 3中使其工作,您需要在check_output调用中添加universal_newlines=True - Brecht Machiels
@BrechtMachiels 如果只是使用 universal_newline=True,那么代码在 Python 2/3 上都会出错。当前的解决方案足够通用,甚至支持无法解码的环境变量值。在 Python 3 上,我可能会对这些值使用 os.fsdecode。我已经更新了答案,明确说明该代码适用于 Python 2。 - jfs

6

source 是一个bash特定的shell内置命令(而非交互式shell通常是轻量级的dash shell而不是bash)。相反,只需调用/bin/sh

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

1
如果 the_script.sh 有适当的 shebang 和权限(+x),那么 foo = subprocess.Popen("./the_script.sh") 应该可以工作。 - jfs

4

更新:2019年

"""
    Sometime you want to emulate the action of "source" in bash,
    settings some environment variables. Here is a way to do it.
"""
def shell_source( str_script, lst_filter ):
    #work around to allow variables with new lines
    #example MY_VAR='foo\n'
    #env -i create clean shell
    #bash -c run bash command
    #set -a optional include if you want to export both shell and enrivonment variables
    #env -0 seperates variables with null char instead of newline
    command = shlex.split(f"env -i bash -c 'set -a && source {str_script} && env -0'")

    pipe = subprocess.Popen( command, stdout=subprocess.PIPE )
    #pipe now outputs as byte, so turn it to utf string before parsing
    output = pipe.communicate()[0].decode('utf-8')
    #There was always a trailing empty line for me so im removing it. Delete this line if this is not happening for you.
    output = output[:-1]

    pre_filter_env = {}
    #split using null char
    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 )

2

由@xApple的答案变化而来,因为有时候可以使用shell脚本(而不是Python文件)来设置环境变量,可能执行其他shell操作,然后传播该环境到Python解释器,而不是在子shell关闭时丢失该信息。

变化的原因是,“env”输出格式中每行一个变量的假设并不完全可靠:我刚刚处理了一个包含换行符的变量(我想是一个shell函数),这搞乱了解析。因此,这里是一个稍微复杂一些的版本,它使用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语句。当然,这里有一个exec,所以要小心不受信任的输入......但我认为这是关于如何源/执行任意shell脚本的讨论中隐含的;)
更新:请参见@unutbu对@xApple答案评论,以获取处理env输出中换行符的替代方法(可能更好)。

1
基于os.environ.update()的方法如果./shellscript.sh取消设置某些变量,则会失败。可以使用os.environ.clear()。您可以使用json.dumps(dict(os.environ))json.loads(output)代替'%r'exec。虽然简单的env -0.split('\0')在这里很有效 - jfs

1

根据这里的答案,我创建了一个适合我的需求的解决方案。

  • 无需过滤环境变量
  • 允许包含换行符的变量
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.
    """
    
    pipe = subprocess.Popen(". %s && env -0" % script, stdout=subprocess.PIPE, shell=True)
    output = pipe.communicate()[0].decode('utf-8')
    output = output[:-1] # fix for index out for range in 'env[ line[0] ] = line[1]'

    env = {}
    # split using null char
    for line in output.split('\x00'):
        line = line.split( '=', 1)
        # print(line)
        env[ line[0] ] = line[1]

    os.environ.update(env)

有了这个,我能够在不出问题的情况下使用相同的环境变量运行命令:

def runCommand(command):
    """
    Prints and then runs shell command.
    """
    print(f'> running: {command}')
    stream = subprocess.Popen(command, shell=True,env=os.environ)
    (result_data, result_error) = stream.communicate()
    print(f'{result_data}, {result_error}')

希望这能帮助和我处于同样情况的人。

0

看起来有很多关于这个问题的答案,我没有全部阅读,所以可能已经有人指出了;但是,在调用像这样的shell命令时,您必须在Popen调用中传递shell=True。否则,您可以调用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')

0
如果您想将source命令应用于其他脚本或可执行文件,则可以创建另一个包装脚本文件,并从中调用“source”命令以及任何其他所需的逻辑。在这种情况下,此source命令将修改其运行的本地上下文 - 即由subprocess.Popen创建的子进程中。
如果您需要修改Python上下文(即程序正在运行的上下文),则此方法将不起作用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接