Python subprocess Popen环境变量PATH是什么?

82

假设有一个可执行文件和一个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个回答

92
相对路径(包含斜杠的路径)无论你做什么,都不会在任何PATH中进行检查。它们仅相对于当前工作目录。如果您需要解析相对路径,则必须手动搜索PATH
如果您想要运行一个程序,相对于Python脚本的位置,请使用__file__,从那里找到程序的绝对路径,然后在Popen中使用绝对路径。
在当前进程的环境变量PATH中搜索
关于Python如何处理裸命令(没有斜杠)有一个Python bug跟踪器问题。基本上,在Unix / Mac上,当参数env = None时,Popen的行为类似于os.execvp(在末尾观察并记录了一些意外的行为):
引用: 在POSIX上,该类使用 os.execvp()类似的行为来执行子程序。
这实际上适用于 shell = False shell = True ,只要 env = None 。此行为的含义在函数os.execvp的文档中有解释:
这些变体(execlp(), execlpe(), execvp(), 和 execvpe())中,如果文件名以“p”结尾,则使用PATH环境变量来定位程序文件。当环境被替换时(使用下一段讨论的exec*e变体之一),新环境将作为PATH变量的来源。
对于execle(), execlpe(), execve(), 和 execvpe()(注意它们都以“e”结尾),env参数必须是一个映射,用于定义新进程的环境变量(这些变量代替当前进程的环境变量);而execl(), execlp(), execv(), 和 execvp()函数会导致新进程继承当前进程的环境。
第二段引用的意思是,execvp将使用当前进程的环境变量。结合第一段引用,我们可以推断出execvp将从当前进程的环境中使用环境变量PATH的值。这意味着Popen会查看Python启动时(运行Popen实例的Python)PATH的值,并且无论更改多少次os.environ都不能解决这个问题。
此外,在Windows上,如果shell=FalsePopen根本不关心PATH,并且只会在相对于当前工作目录中查找。

shell=True的作用

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

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

That is to say, Popen does the equivalent of:

Popen(['/bin/sh', '-c', args[0], args[1], ...])
换句话说,使用shell=True,Python将直接执行/bin/sh,而不进行任何搜索(通过将参数executable传递给Popen可以更改此设置,如果它是没有斜杠的字符串,则Python将其解释为要在当前进程环境的PATH值中搜索的shell程序名称,即在shell=False的情况下搜索程序的方式)。
反过来,/bin/sh(或我们的shell executable)将在其自己的环境的PATH中查找要运行的程序,这与Python(当前进程)的PATH相同,根据上面“也就是说...”后面的代码推断出来(因为该调用具有shell=False,所以已经在前面讨论过了)。 因此,只要env=None,我们就会获得类似于execvp的行为,无论是shell=True还是shell=False

Popen传递env

那么,如果我们将env=dict(PATH=...)传递给Popen(从而在由Popen运行的程序的环境中定义了一个环境变量PATH),会发生什么?
在这种情况下,新环境用于搜索要执行的程序。引用Popen的文档:
如果env不是None,则必须是定义新进程环境变量的映射;这些变量代替了继承当前进程环境的默认行为。
结合上述观察和使用Popen进行实验,这意味着在这种情况下,Popen的行为类似于函数os.execvpe。如果shell=False,Python会在新定义的PATH中查找给定程序。如上所述,对于shell=True,在这种情况下,程序要么是/bin/sh,要么是使用参数executable给定的程序名,然后在新定义的PATH中搜索此替代(shell)程序。
此外,如果shell=True,则在shell内部,shell将用于查找args中给定的程序的搜索路径是通过env传递给PopenPATH的值。
因此,当env != None时,PopenenvPATH键的值中搜索(如果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], ...)

这样可以通过选项-Penv进程传递不同的PATH,以便找到程序。它还避免了与shell元字符和通过shell传递参数时可能出现的安全问题。显然,在Windows上(几乎是唯一没有/usr/bin/env的平台),您需要采取不同的方法。

关于shell=True

引用Popen文档:

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

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

意外观察结果

观察到以下行为:

  • This call raises FileNotFoundError, as expected:

    subprocess.call(['sh'], shell=False, env=dict(PATH=''))
    
  • This call finds sh, which is unexpected:

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

    Typing echo $PATH inside the shell that this opens reveals that the PATH value is not empty, and also different from the value of PATH in the environment of Python. So it seems that PATH was indeed not inherited from Python (as expected in the presence of env != None), but still, it the PATH is nonempty. Unknown why this is the case.

  • This call raises FileNotFoundError, as expected:

    subprocess.call(['tree'], shell=False, env=dict(FOO=''))
    
  • This finds tree, as expected:

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

15
“另外,在Windows上如果shell=False,它根本不会理会PATH环境变量,只会查找相对于当前工作目录的路径。” 这段话帮助我解决了一个大问题 - 谢谢! - sparc_spread
3
一个在Windows上同样有效的简单方法是将os.environ['PATH']显式地作为参数env传递给subprocess.Popen,就像这里所做的一样:https://dev59.com/oXE95IYBdhLWcg3wn_Rr#4453495和这里所做的一样:https://dev59.com/6GIj5IYBdhLWcg3wERNx#20669704。 - 0 _
“/usr/bin/env” 技巧不起作用,至少对于像 “useradd” 这样的系统命令,在 CentOS 上(使用 cron 的空 PATH): “/usr/bin/env: groupadd: No such file or directory”。 - grandrew
如果PATH为空,那并不奇怪。据我所知,与shell不同,/usr/bin/env没有默认的PATH可供回退。老实说,我也不建议依赖shell的默认PATH;如果你正在编写cron作业,只需编写二进制文件的完整路径或自己设置一个PATH即可。 - Walter Mundt
我有一个subprocess.Popen,它似乎使用shell=False搜索路径。然而,无效的是使用sys.path.append增加路径以包括可执行文件的位置 - 我发现只有在Python程序启动之前,%PATH%包含可执行文件的路径时才有效。 - starfry
我得到了usr/bin/env: 无效选项 -- 'P',在env帮助中我没有看到P选项。 - undefined

15

您似乎对 PATHPYTHONPATH 的性质有点困惑。

PATH 是一个环境变量,告诉操作系统 shell 在哪里搜索可执行文件。

PYTHONPATH 是一个环境变量,告诉 Python 解释器在哪里搜索要导入的模块。它与 subprocess 找到可执行文件无关。

由于底层实现的差异,在非 Windows 系统上,默认情况下,subprocess.Popen 只会搜索路径(Windows 有一些系统目录它始终搜索,但这与 PATH 处理不同)。扫描路径的唯一可靠跨平台方式是向 subprocess 调用传递 shell=True,但这有自己的问题(详见 Popen 文档)。

然而,您的主要问题似乎是您将一个路径片段传递给了 Popen,而不是一个简单的文件名。只要在其中有目录分隔符,即使在非 Windows 平台上,您也将禁用 PATH 搜索(例如,请参阅 Linux 的 exec 函数族的文档)。


3
这与Python文档不符。Popen文档指出,程序是通过os.execvp执行的--而该调用确实考虑了PATH环境变量。此外,如果你只需要路径评估,我建议使用env而不是shell=True,如Popen(['/usr/bin/env', 'progtorun', other, args],...)。这避免了与Shell元字符相关的问题和通过Shell传递参数时可能存在的安全问题。 - Walter Mundt
1
这两个都是 *NIX 特定的,它们在 Windows 上不起作用,所以我不喜欢将它们推荐为名义上跨平台的模块的解决方法。尽管如此,你说得对,我的回答写得不正确,我会进行编辑。 - ncoghlan
2
更新内容以明确默认情况下不搜索 PATH 是仅适用于 Windows 的事情,但也指出真正的问题(要执行的命令中的目录分隔符)。 - ncoghlan
一个小修改。subprocess.Popen将会在C:\Windows\System32中选择可执行文件(我很开心能够找到这个问题),如果你在64位的Windows上运行32位的Python,实际上是在C:\Windows\SysWOW64中。 - John Oxley
1
@JohnOxley 我已经修改了答案并提到了这一点,但你是否知道有关此的任何好的参考链接?也许在 MSDN 上可以找到? - ncoghlan
就我个人而言,我在Windows和路径方面没有遇到任何问题...不知道为什么。例如,在我们的Jenkins虚拟环境Windows实例中,完全可以在路径中找到“nosetests”。 - Erik Aronesty

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()

什么?相对于“/dir”的“subdir2/some_script.py”就是“/dir/subdir2/some_script.py”。 - tripleee
位于subdir2/some_script.py的Python脚本使用可执行路径为../subdir1/some_executablePopen进行执行。该可执行路径相对于当前工作目录/dir进行解析,结果为/dir/../subdir1/some_executable。请参阅Walter的答案,他用不同的方式表达了相同的意思。我本可以更好地表达我的答案。谢谢! 编辑:看起来我的答案中也有一个错别字,在可执行路径中使用了subdir2,而我实际上是指subdir1 - Jeremy Fishman

0

PythonPath被设置为执行Python解释器的路径。因此,在您的示例的第二种情况中,路径被设置为/dir而不是/dir/subdir2,这就是为什么您会收到错误信息。


我的意思是说,执行Python的目录会被添加到Python路径中。在这里,第二种情况添加了/dir而不是/dir/subdir2。因此,您可以更改代码以反映这些更改(一种方法是在您的代码中将/dir/subdir2添加到os.path中),或者从适当的目录启动Python。 - c0da

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