如何使使用call/Popen调用的子进程继承环境变量?

17

首先,对于显然是我对bash、shell和子进程的基础理解不足,我表示歉意。

我正在尝试使用Python自动调用一个名为Freesurfer(实际上,我调用的子程序叫做recon-all)的程序。

如果我直接在命令行中执行此操作,我会“源”一个名为mySetUpFreeSurfer.sh的脚本,该脚本仅设置三个环境变量,然后“源”另一个脚本,FreeSurferEnv.sh。 FreeSurferEnv.sh 对我来说似乎只是设置了许多环境变量并在终端上打印一些东西,但它比其他bash脚本复杂,所以我不确定。

这是我现在拥有的:

from subprocess import Popen, PIPE, call, check_output
import os

root = "/media/foo/"

#I got this function from another Stack Overflow question.

def source(script, update=1):
    pipe = Popen(". %s; env" % script, stdout=PIPE, shell=True)
    data = pipe.communicate()[0]
    env = dict((line.split("=", 1) for line in data.splitlines()))
    if update:
        os.environ.update(env)
    return env

source('~/scripts/mySetUpFreeSurfer.sh')
source('/usr/local/freesurfer/FreeSurferEnv.sh')

for sub_dir in os.listdir(root):
    sub = "s" + sub_dir[0:4]
    anat_dir = os.path.join(root, sub_dir, "anatomical")
    for directory in os.listdir(anat_dir):
        time_dir = os.path.join(anat_dir, directory)
        for d in os.listdir(time_dir):
            dicoms_dir = os.path.join(time_dir, d, 'dicoms')
            dicom_list = os.listdir(dicoms_dir)
            dicom = dicom_list[0]
            path = os.path.join(dicoms_dir, dicom)
            cmd1 = "recon-all -i " + path + " -subjid " + sub
            check_output(cmd1, shell=True)
            call(cmd1, shell=True)
            cmd2 = "recon-all -all -subjid " + sub,
            call(cmd2, shell=True)

这里出现了错误:

Traceback (most recent call last):
     File "/home/katie/scripts/autoReconSO.py", line 28, in <module>
        check_output(cmd1, shell=True)
      File "/usr/lib/python2.7/subprocess.py", line 544, in check_output
        raise CalledProcessError(retcode, cmd, output=output)
    CalledProcessError: Command 'recon-all -i /media/foo/bar -subjid s1001' returned non-zero exit status 127

我可能理解为什么会这样。我的脚本后面的"调用"是启动新子进程,这些进程不会从调用source()函数时创建的进程中继承环境变量。我已经尝试了很多方法来确认我的理解。例如,我加入了以下这几行代码:

mkdir ~/testFreeSurferEnv
export TEST_ENV_VAR=~/testFreeSurferEnv

在FreeSurferEnv.sh脚本中。目录的创建很好,但在Python脚本中,这样:

cmd = 'mkdir $TEST_ENV_VAR/test'
check_output(cmd, shell=True)

失败的样子是这样的:

File "/usr/lib/python2.7/subprocess.py", line 544, in check_output
    raise CalledProcessError(retcode, cmd, output=output)
CalledProcessError: Command 'mkdir $TEST_ENV_VAR/test' returned non-zero exit status 1

问题:

如何让运行“recon-all”的子进程继承它所需的环境变量?或者我应该怎样做才能在同一进程中运行设置环境变量的脚本和调用“recon-all”?或者我应该采用另一种方法来解决这个问题?还是我可能误解了问题?


调用subprocess.Popen中的“source”命令 - jfs
2个回答

24

如果你查看Popen文档,它有一个env参数:

如果env不是None,则必须是一个映射,用于定义新进程的环境变量; 这些环境变量代替继承当前进程环境,这是默认行为。

你编写了一个从你的源脚本中提取所需环境并将其放入dict中的函数。只需将结果作为env传递给要使用它的脚本即可。例如:

env = {}
env.update(os.environ)
env.update(source('~/scripts/mySetUpFreeSurfer.sh'))
env.update(source('/usr/local/freesurfer/FreeSurferEnv.sh'))

# …

check_output(cmd, shell=True, env=env)

你所提出的观点非常有道理。显然我没有理解那个函数,并且不知何故认为它作为一个副作用会更新环境变量。然而,我的问题还没有解决。我得到了env['TEST_ENV_VAR']的值等于~/testFreeSurferEnv。到目前为止一切顺利。但是,使用check_output(cmd, shell=True, env=env)仍然会出现相同的错误。使用call(cmd, shell=True, env=env)和Popen(cmd, shell=True, env=env)不会抛出任何异常,但它们也不会创建测试子目录。 - Katie
@Katie: mkdir 失败可能有其他原因。如果您尝试运行 check_output('echo $TEST_ENV_VAR/test', shell=True, env=env),您是否会收到 ~/testFreeSurferEnv/test 或者也失败了? 当我在 OS X 和 Linux 上尝试测试(显式设置 env['TEST_ENV_VAR'] 而不是通过您的其他所有代码,因为您说所有其他代码都有效)使用 Python 2.7 和 3.3 或 3.4,在每种情况下都可以工作。 - abarnert
这并不是失败。我认为我倾向于认为我的问题没有解决,因为昨晚当我尝试以正确的方式使用那个源函数时,recon-all命令仍然以退出状态127失败。可悲的是,我现在无法真正调查这个问题,因为我使用了unutbu建议的“使用Python编写单个bash脚本”的方法来启动我的作业,此时更改环境变量可能会造成混乱。我很想了解更深层次的情况,但目前我必须放手不管。非常感谢您的帮助。 - Katie
有没有直接从子进程中提取所有环境变量的方法?@unutbu的建议相当不错,但我希望有一个内置的方法。 - JDong
@JDong 这是Unix(以及Windows NT)设计的限制。 - abarnert

5

关于

如果我直接在命令行上执行此操作,我会“source”一个名为mySetUpFreeSurfer.sh的脚本,该脚本仅设置三个环境变量,然后“source”另一个脚本FreeSurferEnv.sh。

我认为您最好使用Python自动化编写一个shell脚本newscript.sh的过程,并使用一个调用subprocess.check_output来调用此脚本,而不是多次调用Popencheck_outputcall等函数:

newscript.sh:

#!/bin/bash
source ~/scripts/mySetUpFreeSurfer.sh
source /usr/local/freesurfer/FreeSurferEnv.sh
recon-all -i /media/foo/bar -subjid s1001
...

然后调用

subprocess.check_output(['newscript.sh'])

import subprocess
import tempfile
import os
import stat


with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
    f.write('''\
#!/bin/bash
source ~/scripts/mySetUpFreeSurfer.sh
source /usr/local/freesurfer/FreeSurferEnv.sh
''')
    root = "/media/foo/"
    for sub_dir in os.listdir(root):
        sub = "s" + sub_dir[0:4]
        anat_dir = os.path.join(root, sub_dir, "anatomical")
        for directory in os.listdir(anat_dir):
            time_dir = os.path.join(anat_dir, directory)
            for d in os.listdir(time_dir):
                dicoms_dir = os.path.join(time_dir, d, 'dicoms')
                dicom_list = os.listdir(dicoms_dir)
                dicom = dicom_list[0]
                path = os.path.join(dicoms_dir, dicom)
                cmd1 = "recon-all -i {}  -subjid {}\n".format(path, sub)
                f.write(cmd1)
                cmd2 = "recon-all -all -subjid {}\n".format(sub)
                f.write(cmd2)

filename = f.name
os.chmod(filename, stat.S_IRUSR | stat.S_IXUSR)
subprocess.call([filename])
os.unlink(filename)

顺便提一下,

def source(script, update=1):
    pipe = Popen(". %s; env" % script, stdout=PIPE, shell=True)
    data = pipe.communicate()[0]
    env = dict((line.split("=", 1) for line in data.splitlines()))
    if update:
        os.environ.update(env)
    return env

已损坏。例如,如果script包含以下内容

VAR=`ls -1`
export VAR

那么。
. script; env

可能会返回类似以下的输出。
VAR=file1
file2
file3

这将导致source(script)引发ValueError

env = dict((line.split("=", 1) for line in data.splitlines()))
ValueError: dictionary update sequence element #21 has length 1; 2 is required

有一种方法可以修复source命令:使用零字节而不是模棱两可的换行符分隔env环境变量:

def source(script, update=True):
    """
    http://pythonwise.blogspot.fr/2010/04/sourcing-shell-script.html (Miki Tebeka)
    https://dev59.com/7HA75IYBdhLWcg3wDUYo#QKafEYcBWogLw_1bD41z (ahal)
    """
    import subprocess
    import os
    proc = subprocess.Popen(
        ['bash', '-c', 'set -a && source {} && env -0'.format(script)], 
        stdout=subprocess.PIPE, shell=False)
    output, err = proc.communicate()
    output = output.decode('utf8')
    env = dict((line.split("=", 1) for line in output.split('\x00') if line))
    if update:
        os.environ.update(env)
    return env

无论是否可修复,构建一个复合型的 shell 脚本(如上所示)仍然比解析 env 并将 env 字典传递给 subprocess 调用更加优秀。


我要试一下!只是逐个尝试解决方案。 - Katie
这个很好用,非常感谢!我将永远记住只使用Python编写单个bash脚本的技巧,并且从您的答案中学到了一些新的Python知识。也许您应该注意这里源函数是如何破损的:https://dev59.com/Rmw05IYBdhLWcg3wxkmr#12708396?那是我找到它的地方。 - Katie

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