从Python脚本中检查程序是否存在

112

如何在Python脚本中检查程序是否存在?

假设您想要检查wgetcurl是否可用,并且它们应该在路径中。

最好看到一个多平台的解决方案,但现在只需要Linux。

提示:

  • 仅运行命令并检查返回代码并不总是足够的,因为有些工具即使尝试使用--version也会返回非0结果。
  • 在检查命令时屏幕上不应显示任何内容。

另外,我希望得到一个更通用的解决方案,例如is_tool(name)

9个回答

215

shutil.which

推荐一种尚未讨论过的选项:Python实现的which命令,具体来说是shutil.which。它在Python 3.3中被引入,并且跨平台支持Linux、Mac和Windows。它也可以通过whichcraft获得Python 2.x版本。您还可以从whichcraft的这里直接提取which代码并将其插入到您的程序中。

def is_tool(name):
    """Check whether `name` is on PATH and marked as executable."""

    # from whichcraft import which
    from shutil import which

    return which(name) is not None

distutils.spawn.find_executable

另一个已经提到的选择是distutils.spawn.find_executable

find_executable的文档字符串如下:

尝试在“path”中列出的目录中查找“executable”

因此,如果您注意到,您会发现该函数的名称有些误导。与which不同,find_executable 实际上并不验证 executable 是否已标记为可执行,只验证它是否在PATH中。因此,find_executable指示可用程序时(虽然可能不太可能),实际上可能不存在该程序。

例如,假设您有一个未标记为可执行的文件/usr/bin/wget。从shell中运行wget将导致以下错误:bash:/usr/bin/wget:权限被拒绝which('wget') is not None 将返回False,但find_executable('wget') is not None将返回True。您可能可以使用任何一个函数,但这只是需要注意的find_executable的一点。

def is_tool(name):
    """Check whether `name` is on PATH."""

    from distutils.spawn import find_executable

    return find_executable(name) is not None

7
shutil.which() 的文档链接:https://docs.python.org/zh-cn/3/library/shutil.html#shutil.which - Nick Chammas
为distutils.spawn.find_executable点赞!谢谢! - Barmaley
如果Python函数遵循shell命令... which不是一个好选择。你应该使用command -v代替。另请参见如何从Bash脚本中检查程序是否存在?如何在shell脚本中检查命令是否存在? - jww
1
检查源代码 shutil.which,网址:https://github.com/python/cpython/blob/3e986de0d65e78901b55d4e500b1d05c847b6d5e/Lib/shutil.py#L1291 或 https://github.com/pydanny/whichcraft/blob/master/whichcraft.py#L20。它是纯 Python 编写的,不会调用 which 二进制文件,所以那些答案中提出的参数都不相关。 - Six
有人可以帮我理解 return which(name) is not None 这一行吗? - Peter Schorn
@PeterSchorn 的 find_executable(name) 如果找不到任何东西,就会返回 None。因此,在 None is not None 中将会是 False,因此 return False。当它不是 None 时,条件变为 True 并返回 True - Hossein

55

最简单的方法是尝试使用所需参数运行程序,如果不存在,则处理异常:

try:
    subprocess.call(["wget", "your", "parameters", "here"])
except FileNotFoundError:
    # handle file not found error.

这是Python中的一种常见模式:EAFP

在Python 2中,由于操作系统错误的更细粒度的异常类还不存在,您必须捕获OsError异常:

try:
    subprocess.call(["wget", "your", "parameters", "here"])
except OSError as e:
    if e.errno == errno.ENOENT:
        # handle file not found error.
    else:
        # Something else went wrong while trying to run `wget`
        raise

1
@DarenThomas:对于你不知道如何使用的程序,它们是否存在似乎并不太有用。 :) - Sven Marnach
1
你的解决方案将显示执行命令的输出,看看我的解决方案,基于你的解决方案,完全不会有任何输出。https://dev59.com/RWgu5IYBdhLWcg3wloLA#11210902 - sorin
1
@SorinSbarnea:我的方法完全不同。你测试命令是否存在的唯一原因是你计划运行它。我的建议是:不要提前测试,直接运行它,如果出现问题就捕获异常。这不会以任何方式污染stdout或stderr,并且会导致更清洁的代码和更少的开销。 - Sven Marnach
1
至少在 Python 3 中,可能更清晰的方法是直接捕获 FileNotFoundError。可以省略 if 语句。 - fabian789
1
@fabian789 很好的建议,已经更新了答案。 - Sven Marnach
显示剩余7条评论

15

你可以使用子进程调用运行二进制文件,具体方法如下:

  • "which":适用于*nix系统
  • "where":适用于Windows 2003及以后的系统(XP需要安装插件)

使用上述方法可以获取可执行文件路径(假设该文件已在环境路径中)。

import os 
import platform
import subprocess

cmd = "where" if platform.system() == "Windows" else "which"
try: 
    subprocess.call([cmd, your_executable_to_check_here])
except: 
    print "No executable"

或者直接使用Ned Batchelder的wh.py脚本,这是一个跨平台的"which"实现:

http://nedbatchelder.com/code/utilities/wh_py.html


1
当程序不存在时,调用不会抛出异常,而是返回非零值。 - Photon

13

我会使用which wget或者which curl的方式获取相应程序,然后检查结果是否以你正在使用的程序名结尾。Unix的魔力 :)

实际上,你只需要检查which的返回代码即可。因此... 使用我们可靠的subprocess模块:

import subprocess

rc = subprocess.call(['which', 'wget'])
if rc == 0:
    print('wget installed!')
else:
    print('wget missing in path!')
注意,我在Windows上使用cygwin进行了测试...如果你想找出如何在纯Python中实现which,我建议你在这里查看:http://pypi.python.org/pypi/pycoreutils(哦,亲爱的 - 看来他们没有提供which。时间来友善地推一下?) 更新:在Windows上,您可以使用where代替which以获得类似的效果。

1
这不会尝试运行一个名为"which wget"的程序,即文件名中带有空格吗? - Sven Marnach
@SvenMarnach,没错!我把语法都搞错了:( 哎呀。 - Daren Thomas
1
这应该是:subprocess.call(['which', 'wget']) - nbubis
@nbubis,谢谢!我更新了我的答案以反映这一点。 - Daren Thomas
很遗憾,这个解决方案不适用于Windows平台。 - Guillaume Jacquenot
which 不是一个好的选择。你应该使用 command -v。另外参见如何从Bash脚本中检查程序是否存在?如何在shell脚本中检查命令是否存在? - jww

13
import subprocess
import os

def is_tool(name):
    try:
        devnull = open(os.devnull)
        subprocess.Popen([name], stdout=devnull, stderr=devnull).communicate()
    except OSError as e:
        if e.errno == os.errno.ENOENT:
            return False
    return True

3
如果子进程填满了标准输出或标准错误流的管道缓冲区,它将无限期地运行。如果你只是想运行进程来检查它是否存在,你应该使用打开的os.devnull作为标准输出和标准错误流。请注意,在这种情况下,进程不会输出到控制台。 - Sven Marnach
不太可能发生,但你是正确的,谢谢。 - sorin
1
许多工具在没有参数调用时输出使用信息,这很容易填满管道缓冲区。不过我对我的初始评论是错误的 - 我错过了对“communcate()”的调用,它超出了代码框的右边界,而我没有向右滚动足够远。方法“Popen.communicate()”负责避免任何死锁情况。 - Sven Marnach
确保打开/dev/null进行写入,即:open(os.devnull, "w") - yadutaf
3
仅供参考,对于使用 Python 3.3 及更高版本的人们,可以使用 subprocess.DEVNULL 而不是 open(os.devnull) - Six
1
在打开后,您还需要关闭os.devnull。否则,如果执行多次,将会有太多的句柄打开,最终将超过限制。 - Astitva Srivastava

12

我的选择是:

import distutils.spawn

def is_tool(name):
  return distutils.spawn.find_executable(name) is not None

distutils.spawn 在 Linux 和 Mac OS X 上运行良好。但在后者中,如果您制作了一个 app,并双击执行,distutils.spawn 总是返回 None - muammar

4
我会把@sorin的回答更改如下,原因是它会检查程序名称而无需传递程序的绝对路径。
from subprocess import Popen, PIPE

def check_program_exists(name):
    p = Popen(['/usr/bin/which', name], stdout=PIPE, stderr=PIPE)
    p.communicate()
    return p.returncode == 0

2
很遗憾,这个解决方案不适用于Windows平台。 - Guillaume Jacquenot
@GuillaumeJacquenot 感谢您告诉我这个问题,我没有Windows平台可以进行测试。 - Nicole Finnie
which 不具备可移植性,不符合 POSIX 标准,并且在大多数平台上默认未安装。请使用 command -vtype 命令。 - sorin

1
import os
import subprocess


def is_tool(prog):
    for dir in os.environ['PATH'].split(os.pathsep):
        if os.path.exists(os.path.join(dir, prog)):
            try:
                subprocess.call([os.path.join(dir, prog)],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT)
            except OSError, e:
                return False
            return True
    return False

这不是平台无关的:如果你真的想复制操作系统功能,你至少应该使用 os.path.join()os.pathsep - Sven Marnach
(我还编辑了os.pathsep - 在Windows上,PATH是用分号分隔的。) - Sven Marnach
好的发现,我的编辑是否覆盖了你的路径分隔符? - ryanday
我喜欢你的函数,但对于以交互模式运行的程序来说,它的效果并不是很好。例如尝试使用 nslookup - muammar

0

对@SvenMarnach代码的轻微修改,解决了打印到标准输出流的问题。如果您使用subprocess.check_output()函数而不是subprocess.call(),那么您可以在代码中处理通常打印到标准输出的字符串,并仍然捕获异常和退出状态码。

如果您想在终端中抑制标准输出流,请不要打印从check_output返回的std out字符串:

import subprocess
import os
try:
    stdout_string = subprocess.check_output(["wget", "--help"], stderr=subprocess.STDOUT)
    # print(stdout_string)
except subprocess.CalledProcessError as cpe:
    print(cpe.returncode)
    print(cpe.output)
except OSError as e:
    if e.errno == os.errno.ENOENT:
        print(e)
    else:
        # Something else went wrong while trying to run `wget`
        print(e)

非零退出状态码和输出字符串在 CalledProcessError 中作为 subprocess.CalledProcessError.returncodesubprocess.CalledProcessError.output 抛出,因此您可以根据需要进行处理。

如果您想将可执行文件的标准输出打印到终端,请打印返回的字符串:

import subprocess
import os
try:
    stdout_string = subprocess.check_output(["wget", "--help"], stderr=subprocess.STDOUT)
    print(stdout_string)
except subprocess.CalledProcessError as cpe:
    print(cpe.returncode)
    print(cpe.output)
except OSError as e:
    if e.errno == os.errno.ENOENT:
        print(e)
    else:
        # Something else went wrong while trying to run `wget`
        print(e)

print()函数会在字符串末尾添加一个额外的换行符。如果你想要消除这个换行符(并且像上面的print()语句一样将标准错误写入标准错误流而不是标准输出流),可以使用sys.stdout.write(string)sys.stderr.write(string)代替print()

import subprocess
import os
import sys
try:
    stdout_string = subprocess.check_output(["bogus"], stderr=subprocess.STDOUT)
    sys.stdout.write(stdout_string)
except subprocess.CalledProcessError as cpe:
    sys.stderr.write(cpe.returncode)
    sys.stderr.write(cpe.output)
except OSError as e:
    if e.errno == os.errno.ENOENT:
        sys.stderr.write(e.strerror)
    else:
        # Something else went wrong while trying to run `wget`
        sys.stderr.write(e.strerror)

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