从subprocess.Popen调用“source”命令

时间:2011-08-12 13:13:11

标签: python unix popen

我有一个用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中调用“source”,当我可以在python之外?

9 个答案:

答案 0 :(得分:27)

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

答案 1 :(得分:23)

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

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

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

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

答案 2 :(得分:13)

已损坏的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,不理解它({{ 1}}使用/bin/sh)。假设.中可能存在其他bash-ism,它应该使用bash运行:

the_script.sh

作为@IfLoop said,在子进程中执行foo = Popen("source the_script.sh", shell=True, executable="/bin/bash") 并不是非常有用,因为它不会影响父进程的环境。

如果source对某些变量执行os.environ.update(env),则基于the_script.sh的方法会失败。可以调用unset来重置环境:

os.environ.clear()

它使用env -0 and .split('\0') suggested by @unutbu

为了支持#!/usr/bin/env python 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 中的任意字节,可以使用os.environb模块(假设我们使用固定"json.dumps not parsable by json.loads" issue的Python版本):

为避免通过管道传递环境,可以更改Python代码以在子进程环境中调用自身,例如:

json

答案 3 :(得分:2)

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

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

答案 4 :(得分:1)

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

变化的原因是“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输出中换行符的替代方法(可能更好)。

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

似乎有很多答案,没有阅读所有这些,所以他们可能已经指出了;但是,当调用这样的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')

答案 7 :(得分:0)

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

答案 8 :(得分:0)

使用此处的答案,我创建了适合自己需求的解决方案。

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

希望这可以帮助与我处于同一职位的任何人