从 subprocess.Popen 调用“源”命令

Posted

技术标签:

【中文标题】从 subprocess.Popen 调用“源”命令【英文标题】:Calling the "source" command from subprocess.Popen 【发布时间】:2011-10-25 19:37:07 【问题描述】:

我有一个使用source the_script.sh 调用的.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 调用“源”,而我可以在 python 之外调用?

【问题讨论】:

Emulating Bash 'source' in Python的可能重复 【参考方案1】:

source 不是可执行命令,它是内置的 shell。

使用source 的最常见情况是运行一个改变环境的shell 脚本并在当前shell 中保留该环境。这正是 virtualenv 修改默认 python 环境的方式。

创建子进程并在子进程中使用source 可能不会做任何有用的事情,它不会修改父进程的环境,不会发生使用源脚本的任何副作用.

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

【讨论】:

另请注意,虽然execfile 是完全类似的,但在 Python 程序中,import 几乎总是在您通常在 shell 脚本中使用 source 的地方使用。 有趣。因此,即使我按照 phihag 的建议进行操作,对环境变量的任何更改实际上都不会生效? 好吧,他们会坚持使用 bash 子进程,但是这对你有什么好处取决于 the_script.sh 的实际作用。通过source 调用的脚本不太可能在子进程中发挥很大作用。 source: not found - the issues is that /bin/sh doesn't support but /bin/bash does. 注意:execfile() 在 Python3 中被替换为 exec()【参考方案2】:

您可以只在子 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)

【讨论】:

归功于它:这来自pythonwise.blogspot.fr/2010/04/sourcing-shell-script.html(虽然可能是xApple == Miki?)但请注意:通常脚本参数需要是一个显式路径,即“myenv.sh " 一般不会起作用,但 "./myenv.sh" 会。这是因为在严格执行 sh shell 的系统(例如 Debian/Ubuntu)上的 sourcing 内置 (.) 行为。 @andybuckley 评论正确。使用“./myenv.sh”代替“myenv.sh”。 如果环境变量的值包含换行符,此函数可能会引发ValueError 。要fix,请使用env -0output.split('\x00') @unutbu:我已将您的建议用于support arbitrary bytes except '\0' in environment variables' values 我试过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()中的参数是文件对象还是直接文件名?【参考方案3】:

损坏的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 内置命令(在 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 said,在子进程中执行source并不是很有用,因为它不会影响父进程的环境。

如果the_script.sh 对某些变量执行unset,则基于os.environ.update(env) 的方法会失败。可以调用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 and .split('\0') suggested by @unutbu

为了支持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))

【讨论】:

为了在 Python 3 中工作,您需要将 universal_newlines=True 添加到 check_output 调用中。 @BrechtMachiels universal_newline=True 是错误的(它会破坏 Python 2/3 上的代码)。当前的解决方案足够通用,甚至可以支持不可解码的 envvar 值。在 Python 3 上,我可能会在值上使用 os.fsdecode。我已经更新了答案,明确表明代码适用于 Python 2。【参考方案4】:

source 是内置的特定于 bash 的 shell(非交互式 shell 通常是轻量级的 dash shell 而不是 bash)。相反,只需致电/bin/sh

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

【讨论】:

如果 the_script.sh 有适当的 shebang 和权限 (+x) 那么 foo = subprocess.Popen("./the_script.sh") 应该可以工作。【参考方案5】:

更新: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 )

【讨论】:

【参考方案6】:

@xApple 回答的一个变体,因为有时能够获取 shell 脚本(而不是 Python 文件)来设置环境变量,并可能执行其他 shell 操作,然后将该环境传播到Python 解释器,而不是在子 shell 关闭时丢失该信息。

变化的原因是“env”输出的每行一个变量格式的假设并不是 100% 稳健:我只需要处理一个变量(我认为是一个 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's comment on the @xApple answer,了解在env 输出中处理换行符的另一种(可能更好)方法。

【讨论】:

如果./shellscript.sh 取消设置某些变量,os.environ.update()-based 方法将失败。 os.environ.clear() could be used.。你可以用json.dumps(dict(os.environ))json.loads(output) 代替'%r'exec。虽然很简单env -0 and .split('\0') works well here.【参考方案7】:

使用此处的答案,我创建了一个适合我需要的解决方案。

无需过滤掉环境变量 允许使用换行符的变量
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')

希望这对和我处于相同位置的人有所帮助

【讨论】:

【参考方案8】:

如果您想将 source 命令应用到一些其他脚本或可执行文件,那么您可以创建另一个包装脚本文件并使用您需要的任何进一步逻辑从中调用“source”命令。在这种情况下,此 source 命令将修改它运行的本地上下文 - 即在 subprocess.Popen 创建的子进程中。

如果您需要修改程序运行的 python 上下文,这将不起作用。

【讨论】:

【参考方案9】:

似乎有很多答案,还没有读完,所以他们可能已经指出了;但是,当调用这样的 shell 命令时,您必须将 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')

【讨论】:

以上是关于从 subprocess.Popen 调用“源”命令的主要内容,如果未能解决你的问题,请参考以下文章

如何从 subprocess.Popen 使用 STDIN [重复]

管道输出subprocess.Popen到文件

Python:使用 subprocess.call 获取输出,而不是 Popen [重复]

Python调用linux系统命令--使用subprocess模块

Python 2.7:调用 subprocess.popen 阻止文件访问

当从 Django 内部调用时,Subprocess.Popen 与交互式程序一起挂起