python subprocess Popen 环境 PATH?

83 投票
4 回答
106828 浏览
提问于 2025-04-16 15:43

假设有一个可执行文件和一个用来启动它的Python脚本,它们分别在“兄弟”子目录下,比如说:

/tmp/subdir1/myexecutable
/tmp/subdir2/myscript.py

如果在 /tmp 目录下,运行 python subdir2/myscript.py,并且用相对路径来指向可执行文件

# myscript.py
from subprocess import Popen
proc = Popen(["../subdir1/myexecutable"])

这时会出现 OSError: [Errno 2] No such file or directory 的错误。

那么,Python是怎么寻找这个可执行文件的呢?它是使用当前的工作目录,还是脚本所在的位置?它会用到PATH和PYTHONPATH吗?你能改变 subprocess.Popen 查找可执行文件的方式和位置吗?对于可执行文件,绝对路径和相对路径的处理方式有什么不同吗?在Linux和Windows之间有区别吗?shell=True 或者 shell=False 会有什么影响呢?

4 个回答

2

在使用subprocess.Popen时,相对路径是相对于当前工作目录的,而不是系统PATH中的元素。如果你在/dir目录下运行python subdir2/some_script.py,那么Popen传递的可执行文件的预期位置将是/dir/../subdir1/some_executable,也就是/subdir1/some_executable,而不是/dir/subdir1/some_executable

如果你想从脚本所在的目录使用相对路径来指向某个特定的可执行文件,最好的办法是先从__file__这个全局变量中构建一个绝对路径。

#/usr/bin/env python
from subprocess import Popen, PIPE
from os.path import abspath, dirname, join
path = abspath(join(dirname(__file__), '../subdir1/some_executable'))
spam, eggs = Popen(path, stdout=PIPE, stderr=PIPE).communicate()
16

你似乎对 PATHPYTHONPATH 的概念有点混淆。

PATH 是一个环境变量,它告诉操作系统的命令行在哪里寻找可执行文件。

PYTHONPATH 是另一个环境变量,它告诉 Python 解释器在哪里寻找要导入的模块。它和 subprocess 查找可执行文件没有关系。

由于底层实现的不同,subprocess.Popen 默认只会在非 Windows 系统上搜索路径(Windows 有一些系统目录是它总会搜索的,但这和 PATH 的处理是不同的)。要在不同平台上可靠地扫描路径,唯一的方法是给 subprocess 调用加上 shell=True,但这样也会有一些问题(具体可以查看 Popen 文档)。

不过,看起来你主要的问题是你传给 Popen 的是一个路径片段,而不是简单的文件名。一旦你在里面加了目录分隔符,即使在非 Windows 平台上,也会禁用 PATH 的搜索(例如,可以查看 Linux 文档中关于 exec 系列函数 的内容)。

94

相对路径(包含斜杠的路径)在任何PATH中都不会被检查,无论你怎么做。它们只与当前工作目录有关。如果你需要解析相对路径,你必须手动在PATH中查找。

如果你想要运行一个程序,相对于Python脚本的位置,可以使用__file__来找到程序的绝对路径,然后在Popen中使用这个绝对路径。

在当前进程的环境变量PATH中搜索

在Python的错误跟踪器中有一个关于Python如何处理裸命令(没有斜杠)的问题。基本上,在Unix/Mac上,当参数env=None时,Popen的行为类似于os.execvp(一些意外的行为在最后有说明):

在POSIX系统上,这个类使用os.execvp()的行为来执行子程序。

这实际上对于shell=Falseshell=True都是正确的,只要env=None。这种行为的含义在os.execvp的文档中有解释:

带有“p”结尾的变体(execlp(), execlpe(), execvp(), 和 execvpe())将使用PATH环境变量来定位程序文件。当环境被替换时(使用某个exec*e变体,下一段会讨论),新的环境将作为PATH变量的来源。

对于execle(), execlpe(), execve(), 和 execvpe()(注意这些都以“e”结尾),env参数必须是一个映射,用于定义新进程的环境变量(这些将替代当前进程的环境);execl(), execlp(), execv(), 和 execvp()都会使新进程继承当前进程的环境。

第二段引用的内容暗示execvp将使用当前进程的环境变量。结合第一段引用的内容,我们可以推断execvp将使用当前进程环境中PATH变量的值。这意味着Popen查看的是Python启动时的PATH值(运行Popen实例化的Python),而无论你怎么改变os.environ都无法解决这个问题。

此外,在Windows上,当shell=False时,Popen根本不关注PATH,只会在当前工作目录中查找。

shell=True的作用

如果我们将shell=True传递给Popen会发生什么?在这种情况下,Popen会简单地调用shell

shell参数(默认为False)指定是否使用shell作为要执行的程序。

也就是说,Popen的效果相当于:

Popen(['/bin/sh', '-c', args[0], args[1], ...])

换句话说,使用shell=True时,Python会直接执行/bin/sh,而不进行任何搜索(将executable参数传递给Popen可以改变这一点,如果它是一个没有斜杠的字符串,Python会将其解释为在当前进程的PATH中查找的shell程序的名称,也就是在shell=False的情况下查找程序时的情况)。

然后,/bin/sh(或我们的shellexecutable)会在它自己的环境PATH中查找我们想要运行的程序,这与Python(当前进程)的PATH是相同的,这一点可以从上面“也就是说...”后的代码推断出来(因为那次调用是shell=False,所以是之前讨论过的情况)。因此,只要env=None,我们在shell=Trueshell=False下得到的都是类似execvp的行为。

env传递给Popen

那么如果我们将env=dict(PATH=...)传递给Popen(因此在将要由Popen运行的程序的环境中定义一个环境变量PATH)会发生什么呢?

在这种情况下,将使用新的环境来查找要执行的程序。引用Popen的文档:

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

结合上述观察和使用Popen的实验,这意味着在这种情况下,Popen的行为类似于函数os.execvpe。如果shell=False,Python会在新定义的PATH中查找给定的程序。正如上面讨论的shell=True的情况,如果给定了程序名称作为executable参数,那么这个替代的(shell)程序将在新定义的PATH中查找。

此外,如果shell=True,那么在shell内部,shell将使用通过env传递给PopenPATH值来查找args中给定的程序。

因此,当env != None时,Popen会在envPATH键的值中查找(如果env中存在PATH键)。

传递除PATH以外的环境变量作为参数

关于除PATH以外的环境变量有一个注意事项:如果命令中需要这些变量的值(例如,作为要运行的程序的命令行参数),那么即使这些变量在传递给Popenenv中存在,它们也不会被解释,除非shell=True。这可以通过不改变shell=True来轻松避免:直接将这些值插入到传递给Popenargs列表参数中。(另外,如果这些值来自Python自己的环境,可以使用方法os.environ.get来获取它们的值。)

使用/usr/bin/env

如果你只需要路径评估,并且不想通过shell运行命令,并且你在UNIX上,我建议使用env而不是shell=True,如:

path = '/dir1:/dir2'
subprocess.Popen(['/usr/bin/env', '-P', path, 'progtorun', other, args], ...)

这让你可以将不同的PATH传递给env进程(使用选项-P),它将用来查找程序。它还避免了shell元字符和通过shell传递参数的潜在安全问题。显然,在Windows上(几乎是唯一没有/usr/bin/env的平台),你需要做不同的事情。

关于shell=True

引用Popen的文档:

如果shellTrue,建议将args作为字符串而不是序列传递。

注意:在使用shell=True之前,请阅读安全考虑部分。

意外观察

观察到以下行为:

  • 这个调用抛出FileNotFoundError,这是预期的:

    subprocess.call(['sh'], shell=False, env=dict(PATH=''))
    
  • 这个调用找到了sh,这有点意外:

    subprocess.call(['sh'], shell=False, env=dict(FOO=''))
    

    在这个shell中输入echo $PATH显示PATH值不是空的,并且与Python环境中的PATH值不同。所以看起来PATH确实没有从Python继承(在env != None的情况下是预期的),但PATH仍然是非空的。不知道为什么会这样。

  • 这个调用抛出FileNotFoundError,这是预期的:

    subprocess.call(['tree'], shell=False, env=dict(FOO=''))
    
  • 这个调用找到了tree,这是预期的:

    subprocess.call(['tree'], shell=False, env=None)
    

撰写回答