如何在Python中优雅地调用Bash脚本?

3

我有一个基本的采购功能:

def source(
    fileName = None,
    update   = True
    ):
    pipe = subprocess.Popen(". {fileName}; env".format(
        fileName = fileName
    ), stdout = subprocess.PIPE, shell = True)
    data = pipe.communicate()[0]
    env = dict((line.split("=", 1) for line in data.splitlines()))
    if update is True:
        os.environ.update(env)
    return(env)

当我尝试使用它来源特定的脚本时,我会收到以下错误:
>>> source("/afs/cern.ch/sw/lcg/contrib/gcc/4.8/x86_64-slc6/setup.sh")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in source
ValueError: dictionary update sequence element #51 has length 1; 2 is required

这是由可执行文件env返回的以下行引起的:
BASH_FUNC_module()=() {  eval `/usr/bin/modulecmd bash $*`
}

闭合链括号在第51行。

如何以健壮、明智的方式从Python中调用Bash脚本,以避免出现这样的错误(和其他可能想到的任何错误)?


你为什么要像这样在Python中调用一个shell脚本?你想做什么?将shell变量转换为Python变量吗? - Etan Reisner
但是“Python模块”不就是一个包含Python文件的文件夹吗?(至少有一个名为__init__.py的文件)。我会尝试回答你的问题,但是你所陈述的目标对我来说没有意义。我不知道一个Python脚本如何创建一个Python模块。除非你在脚本中动态地在文件系统中创建文件。 - Alexander Bird
此外(如果我错了,有人可以纠正我),你根本无法通过创建子进程来更改Python进程的环境变量。当子进程调用bash脚本时,只有子进程的环境变量会被更改。因此,在子进程退出后,你的脚本进程将没有任何更改。 - Alexander Bird
感谢您对此事的帮助。安装程序提供了一个基础设施,Python可以与之交互。这不仅仅是确定模块的适当路径的简单问题:在安装过程中,可执行文件和许多其他东西通过特定的模块为C++库提供Python绑定。我正在尝试做的是编写一个Python脚本,该脚本运行基础设施的安装程序,然后导入一个在安装程序后可用且功能正常的模块。 - d3pd
这个问题还有更多的复杂性,但是了解我正在与PyROOT交互可能会有所帮助。我在我的非常基本的尝试中所展示的是在子进程中设置环境并提取该环境的特征以应用于超级进程环境。 - d3pd
显示剩余2条评论
2个回答

1
你看到的这条线是脚本执行以下操作的结果:
module() { eval `/usr/bin/modulecmd bash $*`; }
export -f module

也就是说,它明确地导出了 bash 函数 module,以便子(bash)shell 可以使用它。
从环境变量的格式可以看出,在 shellshock 补丁程序中间,您升级了 bash。我不认为有当前的补丁程序会生成 BASH_FUNC_module()= 而不是 BASH_FUNC_module%%()=,但是我记得在修复的繁忙期间分发了这样的补丁程序。现在事情已经平息,您可能需要再次升级 bash。(如果这是剪切和粘贴错误,请忽略本段。)
而且,我们还可以知道您系统上的 /bin/shbash,假设 module 函数是通过源化 shell 脚本引入的。
也许您应该决定是否关心导出的 bash 函数。您想将 module 导出到您正在创建的环境中,还是忽略它?下面的解决方案只返回它在环境中找到的内容,因此它将包括 module
简而言之,如果您要解析尝试打印环境的某个 shell 命令的输出,则可能会遇到三个可能的问题:
  1. 导出的函数(仅适用于bash),在shellshock补丁前后外观不同,但始终至少包含一个换行符。(它们的值始终以() {开头,因此很容易识别。在shellshock补丁后,它们的名称将是BASH_FUNC_funcname%%,但在野外找到补丁前后的bash之前,您可能不想依赖它们。)
  2. 包含换行符的导出变量。
  3. 有些情况下,导出变量根本没有值。实际上,它们的值为空字符串,但它们可能在环境列表中没有=符号,并且某些实用程序将在没有=的情况下将它们打印出来。

像往常一样,最稳健(甚至可能是最简单)的解决方案是避免解析,但我们可以退而求其次,采用我们自己创建的格式化字符串的解析策略,该字符串经过精心设计以进行解析。

我们可以使用任何具有环境访问权限的编程语言来生成此输出;为了简单起见,我们可以使用Python本身。我们将以非常简单的格式输出环境变量:变量名称(必须是字母数字),后跟等号,后跟值,后跟一个NUL(0)字节(该字节不能出现在值中)。类似以下内容:
from subprocess import Popen, PIPE

# The commented-out line really should not be necessary; it's impossible
# for an environment variable name to contain an =. However, it could
# be replaced with a more stringent check.
prog = ( r'''from os import environ;'''
       + r'''from sys import stdout;'''
       + r'''stdout.write("\0".join("{k}={v}".format(kv)'''
       + r'''                       for kv in environ.iteritems()'''
      #+ r'''                       if "=" not in kv[0]'''
       + r'''            ))'''
       )

# Lots of error checking omitted.    
def getenv_after_sourcing(fn):
  argv = [ "bash"
         , "-c"
         , '''. "{fn}"; python -c '{prog}' '''.format(fn=fn, prog=prog)]
  data = Popen(argv, stdout=PIPE).communicate()[0]
  return dict(kv.split('=', 1) for kv in data.split('\0'))

-1

我认为直接使用bash来设置环境,然后在已经设置好的环境中调用python脚本通常更好。这是利用Unix/Linux核心原则之一:子进程继承父进程的环境副本。

如果我正确理解了您的情况,那么您有一些bash脚本设置了一些环境,您希望在python脚本中拥有该准备好的环境,然后这些python脚本使用该准备好的环境为更多的工具设置一些环境。

我建议采用以下设置:

  1. 一个bash包装器

    • 使用bash脚本设置环境
    • 调用您的python设置脚本(python脚本从bash脚本继承环境)
  2. 您当前的python脚本不包括子进程和环境读取

    • 在由上述bash脚本准备的环境中启动
    • 继续工作以为下一个工具准备环境

这样,您可以在它们各自的“本地环境”中使用每个脚本。

另一种选择是手动将bash脚本转换为python。


这在一般情况下可能或者不可能。在我的情况下,我需要根据一些以编程方式定义的情况从Python加载模块,所以你的方法对此无效。 - Davide

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