Python中使用subprocess模块的call()、check_call()和returncode来查找命令是否存在

21

我已经掌握了如何使用call()来运行命令的技巧:

import subprocess

mycommandline = ['lumberjack', '-sleep all night', '-work all day']
subprocess.call(mycommandline)
这个方法是可行的,但存在一个问题,如果用户的命令路径中没有相应的lumberjack会怎样呢?如果把lumberjack放在Python脚本所在的目录下,则可以正常工作,但是脚本如何知道它应该寻找lumberjack呢?我想,如果出现“command-not-found”错误,那么lumberjack就不在命令路径中,脚本可以尝试找到自己的目录并在其中查找lumberjack,最后警告用户,在两个位置中没有找到lumberjack时将其复制到其中一个位置。我该如何获取错误消息呢?我读到check_call()可以返回错误消息以及有关returncode属性的信息。我找不到如何使用check_call()和returncode的示例,也不知道消息是什么,或者如何判断消息是否为“command-not-found”。
我这样做是否正确?

2
如果命令没有干净地退出(这在不存在的命令中是预期的),则check_call()应该引发错误。 - inspectorG4dget
3
除非别无选择,我建议不要告诉用户复制可执行文件。包管理器几乎总是安装软件的最佳选择。 - Gareth Latty
1
@DaveBrunker:你好像对callcheck_call有点困惑。call已经给你返回代码了——不是作为属性,而是函数的返回值。使用check_call的原因是它会帮你检查返回代码,如果有问题就会引发CalledProcessError异常。无论哪种方法都会因找不到程序而引发OSError(或类似的错误,具体取决于你的Python版本)。 - abarnert
1
@inspectorG4dget:实际上,不存在的命令不应该非正常退出,因为它们甚至无法启动。这就是为什么它们会引发OSError而不是CalledProcessError,即使使用call而不仅仅是check_call也是如此。请参见Theodros Zelleke的答案。 - abarnert
3个回答

26

一个简单的代码片段:

try:
    subprocess.check_call(['executable'])
except subprocess.CalledProcessError:
    pass # handle errors in the called executable
except OSError:
    pass # executable not found

6

subprocess在找不到命令的情况下会抛出一个异常OSError

当找到命令并且subprocess为您运行命令时,结果代码将从命令中返回。标准是代码0表示成功,任何失败都是一些非零错误代码(这种错误代码有所不同,请检查您正在运行的特定命令的文档)。

因此,如果捕获了OSError,则可以处理不存在的命令,如果检查结果代码,则可以找出命令是否成功。

subprocess的好处在于,您可以使其收集stdoutstderr中的所有文本,然后将其丢弃、返回、记录或显示。我经常使用一个包装器来丢弃来自命令的所有输出,除非命令失败,在这种情况下,来自stderr的文本将被输出。

我同意您不应该要求用户复制可执行文件。程序应位于PATH变量中列出的目录中;如果缺少程序,则应安装该程序,或者如果该程序安装在未列在PATH中的目录中,则用户应更新PATH以包括该目录。

请注意,您可以尝试使用各种硬编码的可执行文件路径多次运行subprocess

import os
import subprocess as sp

def _run_cmd(s_cmd, tup_args):
    lst_cmd = [s_cmd]
    lst_cmd.extend(tup_args)
    result = sp.call(lst_cmd)
    return result

def run_lumberjack(*tup_args):
    try:
        # try to run from /usr/local/bin
        return _run_cmd("/usr/local/bin/lumberjack", tup_args)
    except OSError:
        pass

    try:
        # try to run from /opt/forest/bin
        return _run_cmd("/opt/forest/bin/lumberjack", tup_args)
    except OSError:
        pass

    try:
        # try to run from "bin" directory in user's home directory
        home = os.getenv("HOME", ".")
        s_cmd = home + "/bin/lumberjack"
        return _run_cmd(s_cmd, tup_args)
    except OSError:
        pass

    # Python 3.x syntax for raising an exception
    # for Python 2.x, use:  raise OSError, "could not find lumberjack in the standard places"
    raise OSError("could not find lumberjack in the standard places")

run_lumberjack("-j")

编辑:经过一番思考,我决定彻底重写上述内容。只需传递一个位置列表,并使用循环尝试备用位置,这样会更加简洁。但是如果不需要构建用户主目录的字符串,我就不想这样做,所以我只是让在备选列表中放入可调用对象合法。如果您对此有任何疑问,请随时提出。

import os
import subprocess as sp

def try_alternatives(cmd, locations, args):
    """
    Try to run a command that might be in any one of multiple locations.

    Takes a single string argument for the command to run, a sequence
    of locations, and a sequence of arguments to the command.  Tries
    to run the command in each location, in order, until the command
    is found (does not raise OSError on the attempt).
    """
    # build a list to pass to subprocess
    lst_cmd = [None]  # dummy arg to reserve position 0 in the list
    lst_cmd.extend(args)  # arguments come after position 0

    for path in locations:
        # It's legal to put a callable in the list of locations.
        # When this happens, we should call it and use its return
        # value for the path.  It should always return a string.
        if callable(path):
            path = path()

        # put full pathname of cmd into position 0 of list    
        lst_cmd[0] = os.path.join(path, cmd)
        try:
            return sp.call(lst_cmd)
        except OSError:
            pass
    raise OSError('command "{}" not found in locations list'.format(cmd))

def _home_bin():
    home = os.getenv("HOME", ".")
    return os.path.join(home, "bin")

def run_lumberjack(*args):
    locations = [
        "/usr/local/bin",
        "/opt/forest/bin",
        _home_bin, # specify callable that returns user's home directory
    ]
    return try_alternatives("lumberjack", locations, args)

run_lumberjack("-j")

2

哇,太快了!我结合了Theodros Zelleke的简单示例和steveha的函数使用,加上abarnert对OSError的评论和Lattyware关于移动文件的评论:

import os, sys, subprocess

def nameandpath():
    try:
        subprocess.call([os.getcwd() + '/lumberjack']) 
        # change the word lumberjack on the line above to get an error
    except OSError:
        print('\nCould not find lumberjack, please reinstall.\n')
        # if you're using python 2.x, change the () to spaces on the line above

try:
    subprocess.call(['lumberjack'])
    # change the word lumberjack on the line above to get an error
except OSError:
    nameandpath()

我在Mac OS-X(6.8 / Snow Leopard),Debian(Squeeze)和Windows(7)上进行了测试。在这三个操作系统中,它似乎按照我想要的方式工作。我尝试使用check_call和CalledProcessError,但无论我做什么,每次都会出现错误,并且无法使脚本处理这些错误。为了测试脚本,我将名称从“lumberjack”更改为“deadparrot”,因为我的脚本目录中有lumberjack。

你认为这个脚本存在问题吗?


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