python subprocess Popen 环境 PATH?
假设有一个可执行文件和一个用来启动它的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 个回答
在使用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()
你似乎对 PATH
和 PYTHONPATH
的概念有点混淆。
PATH
是一个环境变量,它告诉操作系统的命令行在哪里寻找可执行文件。
PYTHONPATH
是另一个环境变量,它告诉 Python 解释器在哪里寻找要导入的模块。它和 subprocess
查找可执行文件没有关系。
由于底层实现的不同,subprocess.Popen
默认只会在非 Windows 系统上搜索路径(Windows 有一些系统目录是它总会搜索的,但这和 PATH
的处理是不同的)。要在不同平台上可靠地扫描路径,唯一的方法是给 subprocess
调用加上 shell=True
,但这样也会有一些问题(具体可以查看 Popen
文档)。
不过,看起来你主要的问题是你传给 Popen
的是一个路径片段,而不是简单的文件名。一旦你在里面加了目录分隔符,即使在非 Windows 平台上,也会禁用 PATH
的搜索(例如,可以查看 Linux 文档中关于 exec 系列函数 的内容)。
相对路径(包含斜杠的路径)在任何PATH
中都不会被检查,无论你怎么做。它们只与当前工作目录有关。如果你需要解析相对路径,你必须手动在PATH
中查找。
如果你想要运行一个程序,相对于Python脚本的位置,可以使用__file__
来找到程序的绝对路径,然后在Popen
中使用这个绝对路径。
在当前进程的环境变量PATH
中搜索
在Python的错误跟踪器中有一个关于Python如何处理裸命令(没有斜杠)的问题。基本上,在Unix/Mac上,当参数env=None
时,Popen
的行为类似于os.execvp
(一些意外的行为在最后有说明):
在POSIX系统上,这个类使用
os.execvp()
的行为来执行子程序。
这实际上对于shell=False
和shell=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=True
和shell=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
传递给Popen
的PATH
值来查找args
中给定的程序。
因此,当env != None
时,Popen
会在env
的PATH
键的值中查找(如果env
中存在PATH
键)。
传递除PATH
以外的环境变量作为参数
关于除PATH
以外的环境变量有一个注意事项:如果命令中需要这些变量的值(例如,作为要运行的程序的命令行参数),那么即使这些变量在传递给Popen
的env
中存在,它们也不会被解释,除非shell=True
。这可以通过不改变shell=True
来轻松避免:直接将这些值插入到传递给Popen
的args
列表参数中。(另外,如果这些值来自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
的文档:
如果shell为
True
,建议将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)