web-dev-qa-db-fra.com

Python, sous-processus, call (), check_call et returncode pour trouver si une commande existe

J'ai compris comment utiliser call () pour obtenir mon script python pour exécuter une commande:

import subprocess

mycommandline = ['lumberjack', '-sleep all night', '-work all day']
subprocess.call(mycommandline)

Cela fonctionne mais il y a un problème, que se passe-t-il si les utilisateurs n'ont pas de bûcheron dans leur chemin de commande? Cela fonctionnerait si le bûcheron était placé dans le même répertoire que le script python, mais comment le script sait-il qu'il devrait rechercher le bûcheron? Je me suis dit s'il y avait une erreur de commande introuvable) le bûcheron ne serait pas dans le chemin de commande, le script pourrait essayer de comprendre quel est son répertoire et y rechercher un bûcheron et enfin avertir l'utilisateur de copier le bûcheron dans l'un de ces deux endroits s'il ne se trouvait dans aucun des deux. Comment puis-je trouver le message d'erreur? J'ai lu que check_call () peut renvoyer un message d'erreur et quelque chose sur un attribut returncode. Je n'ai pas trouvé d'exemples sur la façon d'utiliser check_call () et returncode, quel serait le message ou comment je pourrais dire si le message est introuvable.

Suis-je même en train de le faire dans le bon sens?

18
Dave Brunker

Wow, c'était rapide! J'ai combiné l'exemple simple de Theodros Zelleke et l'utilisation des fonctions de steveha avec un commentaire d'abarnert sur OSError et un commentaire de Lattyware sur le déplacement de fichiers:

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

Je l'ai testé sur Mac OS-X (6.8/Snow Leopard), Debian (Squeeze) et Windows (7). Il semblait fonctionner comme je le voulais sur les trois systèmes d'exploitation. J'ai essayé d'utiliser check_call et CalledProcessError mais peu importe ce que j'ai fait, j'ai semblé avoir une erreur à chaque fois et je n'ai pas pu obtenir le script pour gérer les erreurs. Pour tester le script, j'ai changé le nom de "bûcheron" en "deadparrot", car j'avais un bûcheron dans le répertoire avec mon script.

Voyez-vous des problèmes avec ce script tel qu'il est écrit?

2
Dave Brunker

Un simple extrait:

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

subprocess lèvera une exception, OSError, lorsqu'une commande n'est pas trouvée.

Lorsque la commande est trouvée et que subprocess exécute la commande pour vous, le code de résultat est renvoyé par la commande. La norme est que le code 0 signifie succès, et tout échec est un code d'erreur différent de zéro (qui varie; consultez la documentation pour la commande spécifique que vous exécutez).

Ainsi, si vous interceptez OSError, vous pouvez gérer la commande inexistante et si vous vérifiez le code de résultat, vous pouvez savoir si la commande a réussi ou non.

La grande chose à propos de subprocess est que vous pouvez lui faire collecter tout le texte de stdout et stderr, et vous pouvez ensuite le supprimer ou le retourner ou le connecter ou l'afficher comme vous voulez. J'utilise souvent un wrapper qui rejette toutes les sorties d'une commande, sauf si la commande échoue, auquel cas le texte de stderr est sorti.

Je suis d'accord que vous ne devriez pas demander aux utilisateurs de copier des exécutables. Les programmes doivent se trouver dans un répertoire répertorié dans la variable PATH; s'il manque un programme, il doit être installé, ou s'il est installé dans un répertoire qui ne se trouve pas sur le PATH, l'utilisateur doit mettre à jour le PATH pour inclure ce répertoire.

Notez que vous avez la possibilité d'essayer subprocess plusieurs fois avec divers chemins codés en dur vers les exécutables:

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

EDIT: Après y avoir réfléchi un peu, j'ai décidé de réécrire complètement ce qui précède. Il est beaucoup plus propre de simplement passer une liste d'emplacements et de faire une boucle pour essayer les emplacements alternatifs jusqu'à ce que l'un fonctionne. Mais je ne voulais pas créer la chaîne pour le répertoire personnel de l'utilisateur si elle n'était pas nécessaire, j'ai donc rendu légal de mettre un appelable dans la liste des alternatives. Si vous avez des questions à ce sujet, posez-les.

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")
5
steveha