(填写评论中的详细信息以单独回答)
首先,无论您做什么,都不会在任何PATH 中检查相对路径(包含斜杠的路径)。它们仅相对于当前工作目录。如果您需要解析相对路径,则必须手动搜索 PATH,或者使用 PATH 包含子目录,然后按照我的建议使用命令名称。
如果你想运行一个程序相对于Python脚本的位置,使用__file__并从那里去寻找程序的绝对路径,然后在Popen.
在当前进程的环境变量PATH中搜索
其次,an issue in the Python bug tracker 关于 Python 如何处理裸命令(没有斜线)。基本上,在 Unix/Mac 上,Popen 的行为类似于 os.execvp,当参数 env=None 时(观察到一些意外行为并在末尾注明):
在 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 会查看PATH 的值就像Python 启动时的样子(运行Popen 实例化的Python)并且没有多少更改os.environ 将帮助您修复那个。
另外,在带有shell=False 的Windows 上,Popen 根本不关注PATH,只会相对于当前工作目录查找。
shell=True 做了什么
如果我们将shell=True 传递给Popen,会发生什么?在那种情况下,Popen simply calls the shell:
shell 参数(默认为False)指定是否使用shell 作为程序来执行。
也就是说,Popen 相当于:
Popen(['/bin/sh', '-c', args[0], args[1], ...])
也就是说,有了shell=True,Python会直接执行/bin/sh,不需要任何搜索(将参数executable传递给Popen可以改变这一点,看来如果是不带斜线的字符串,那么它将被 Python 解释为 shell 程序的名称,以便在当前进程的环境中搜索 PATH 的值,即在上面描述的 shell=False 的情况下搜索程序。
反过来,/bin/sh(或者我们的shellexecutable)会在自己环境的PATH中寻找我们想要运行的程序,这和Python的PATH是一样的(当前进程) ,从上面的短语“也就是说...”之后的代码中推断出来(因为该调用具有shell=False,所以前面已经讨论过这种情况)。因此,execvp-like 行为是我们使用shell=True 和shell=False 得到的,只要env=None。
将env 传递给Popen
那么如果我们将env=dict(PATH=...) 传递给Popen(从而在Popen 运行的程序的环境中定义一个环境变量PATH)会发生什么?
在这种情况下,新环境用于搜索要执行的程序。引用Popen的文档:
如果env不是None,则必须是为新进程定义环境变量的映射;这些用于代替继承当前进程环境的默认行为。
结合上述观察结果,以及使用Popen 的实验,这意味着在这种情况下Popen 的行为类似于函数os.execvpe。如果shell=False,Python 在新定义的PATH 中搜索给定程序。正如上面已经讨论过的shell=True,在这种情况下程序是/bin/sh,或者,如果程序名称带有参数executable,则在新定义的@ 中搜索这个替代(shell)程序987654407@.
另外,如果shell=True,那么在shell内部shell用来查找args中给出的程序的搜索路径是传递给@987654411的PATH的值@通过env。
因此,使用env != None,Popen 在 env 的键 PATH 的值中搜索(如果 env 中存在键 PATH)。
将PATH 以外的环境变量作为参数传播
关于PATH以外的环境变量有一个警告:如果命令中需要这些变量的值(例如,作为正在运行的程序的命令行参数),那么即使这些变量存在于env 给 Popen,如果没有 shell=True,它们将不会被解释。
无需更改shell=True 即可轻松避免这种情况:将这些值直接插入list 参数args 给Popen。 (另外,如果这些值来自 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之前阅读Security Considerations部分。
意外观察
观察到以下行为:
-
此调用引发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)