115 votes

Comment analyser plusieurs sous-commandes imbriquées en utilisant python argparse ?

Je suis en train d'implémenter un programme en ligne de commande dont l'interface est la suivante :

cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]

J'ai parcouru les argparse documentation . Je peux mettre en œuvre GLOBAL_OPTIONS comme argument facultatif en utilisant add_argument en argparse . Et le {command [COMMAND_OPTS]} en utilisant Sous-commandes .

D'après la documentation, il semble que je ne puisse avoir qu'une seule sous-commande. Mais comme vous pouvez le voir, je dois implémenter une ou plusieurs sous-commandes. Quelle est la meilleure façon d'analyser de tels arguments de ligne de commande en utilisant argparse ?

41voto

Xiongjun Liang Points 81

Je me suis posé la même question, et il semble que j'ai obtenu une meilleure réponse.

La solution consiste à ne pas simplement imbriquer un sous-analysateur dans un autre sous-analysateur, mais à ajouter un sous-analysateur qui suit un autre sous-analysateur.

Le code vous explique comment :

parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--user', '-u',
                    default=getpass.getuser(),
                    help='username')
parent_parser.add_argument('--debug', default=False, required=False,
                        action='store_true', dest="debug", help='debug flag')
main_parser = argparse.ArgumentParser()
service_subparsers = main_parser.add_subparsers(title="service",
                    dest="service_command")
service_parser = service_subparsers.add_parser("first", help="first",
                    parents=[parent_parser])
action_subparser = service_parser.add_subparsers(title="action",
                    dest="action_command")
action_parser = action_subparser.add_parser("second", help="second",
                    parents=[parent_parser])

args = main_parser.parse_args()

28voto

Vikas Points 3756

@mgilson a une belle répondre à cette question. Mais le problème avec le découpage de sys.argv par moi-même est que je perds tous les messages d'aide que Argparse génère pour l'utilisateur. J'ai donc fini par faire ceci :

import argparse

## This function takes the 'extra' attribute from global namespace and re-parses it to create separate namespaces for all other chained commands.
def parse_extra (parser, namespace):
  namespaces = []
  extra = namespace.extra
  while extra:
    n = parser.parse_args(extra)
    extra = n.extra
    namespaces.append(n)

  return namespaces

argparser=argparse.ArgumentParser()
subparsers = argparser.add_subparsers(help='sub-command help', dest='subparser_name')

parser_a = subparsers.add_parser('command_a', help = "command_a help")
## Setup options for parser_a

## Add nargs="*" for zero or more other commands
argparser.add_argument('extra', nargs = "*", help = 'Other commands')

## Do similar stuff for other sub-parsers

Après la première analyse, toutes les commandes enchaînées sont stockées dans le fichier extra . Je le reparse tant qu'il n'est pas vide pour récupérer toutes les commandes enchaînées et créer des espaces de noms distincts pour elles. Et j'obtiens une chaîne d'utilisation plus agréable que celle générée par argparse.

18voto

hpaulj Points 6132

parse_known_args renvoie un espace de noms et une liste de chaînes de caractères inconnues. Ceci est similaire à la méthode extra dans la réponse vérifiée.

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo')
sub = parser.add_subparsers()
for i in range(1,4):
    sp = sub.add_parser('cmd%i'%i)
    sp.add_argument('--foo%i'%i) # optionals have to be distinct

rest = '--foo 0 cmd2 --foo2 2 cmd3 --foo3 3 cmd1 --foo1 1'.split() # or sys.argv
args = argparse.Namespace()
while rest:
    args,rest =  parser.parse_known_args(rest,namespace=args)
    print args, rest

produit :

Namespace(foo='0', foo2='2') ['cmd3', '--foo3', '3', 'cmd1', '--foo1', '1']
Namespace(foo='0', foo2='2', foo3='3') ['cmd1', '--foo1', '1']
Namespace(foo='0', foo1='1', foo2='2', foo3='3') []

Une autre solution consisterait à donner à chaque sous-parseur son propre espace de noms. Cela permet le chevauchement des noms de positionnaires.

argslist = []
while rest:
    args,rest =  parser.parse_known_args(rest)
    argslist.append(args)

17voto

MacFreek Points 391

La solution proposée par @Vikas échoue pour les arguments optionnels spécifiques à la sous-commande, mais l'approche est valide. Voici une version améliorée :

import argparse

# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='foo help')
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

# create the parser for the "command_a" command
parser_a = subparsers.add_parser('command_a', help='command_a help')
parser_a.add_argument('bar', type=int, help='bar help')

# create the parser for the "command_b" command
parser_b = subparsers.add_parser('command_b', help='command_b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')

# parse some argument lists
argv = ['--foo', 'command_a', '12', 'command_b', '--baz', 'Z']
while argv:
    print(argv)
    options, argv = parser.parse_known_args(argv)
    print(options)
    if not options.subparser_name:
        break

Il s'agit d'utiliser parse_known_args au lieu de parse_args . parse_args abandonne dès qu'un argument inconnu de l'analyseur secondaire actuel est rencontré, parse_known_args les renvoie en tant que deuxième valeur dans le tuple renvoyé. Dans cette approche, les arguments restants sont à nouveau transmis à l'analyseur. Ainsi, pour chaque commande, un nouvel espace de noms est créé.

Notez que dans cet exemple de base, toutes les options globales sont ajoutées au premier espace de nommage des options uniquement, et non aux espaces de nommage suivants.

Cette approche fonctionne bien dans la plupart des cas, mais elle présente trois limites importantes :

  • Il n'est pas possible d'utiliser le même argument facultatif pour différentes sous-commandes, comme par exemple myprog.py command_a --foo=bar command_b --foo=bar .
  • Il n'est pas possible d'utiliser des arguments positionnels de longueur variable avec des sous-commandes ( nargs='?' o nargs='+' o nargs='*' ).
  • Tout argument connu est analysé, sans être interrompu par la nouvelle commande. Par exemple, dans PROG --foo command_b command_a --baz Z 12 avec le code ci-dessus, --baz Z sera consommé par command_b et non par command_a .

Ces limitations sont des limitations directes d'argparse. Voici un exemple simple qui montre les limitations de argparse -même en utilisant une seule sous-commande- :

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('spam', nargs='?')
subparsers = parser.add_subparsers(help='sub-command help', dest='subparser_name')

# create the parser for the "command_a" command
parser_a = subparsers.add_parser('command_a', help='command_a help')
parser_a.add_argument('bar', type=int, help='bar help')

# create the parser for the "command_b" command
parser_b = subparsers.add_parser('command_b', help='command_b help')

options = parser.parse_args('command_a 42'.split())
print(options)

Cela augmentera la error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b') .

La cause en est que la méthode interne argparse.ArgParser._parse_known_args() il est trop gourmand et part du principe que command_a est la valeur de l'option spam argument. En particulier, lors de la "séparation" des arguments optionnels et positionnels, _parse_known_args() ne prend pas en compte les noms des arguments (comme command_a o command_b ), mais seulement à l'endroit où ils apparaissent dans la liste des arguments. Elle suppose également que toute sous-commande consommera tous les arguments restants. Cette limitation de argparse empêche également la mise en œuvre correcte de sous-ensembles multi-commandes. Cela signifie malheureusement qu'une implémentation correcte nécessite une réécriture complète de l'élément argparse.ArgParser._parse_known_args() ce qui représente plus de 200 lignes de code.

Compte tenu de ces limitations, il peut être judicieux de revenir à un argument unique à choix multiples au lieu de sous-commandes :

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--bar', type=int, help='bar help')
parser.add_argument('commands', nargs='*', metavar='COMMAND',
                 choices=['command_a', 'command_b'])

options = parser.parse_args('--bar 2 command_a command_b'.split())
print(options)
#options = parser.parse_args(['--help'])

Il est même possible de lister les différentes commandes dans les informations d'utilisation, voir ma réponse https://stackoverflow.com/a/49999185/428542

6voto

Andrzej Points 40

En améliorant la réponse de @mgilson, j'ai écrit une petite méthode d'analyse qui divise argv en parties et place les valeurs des arguments des commandes dans la hiérarchie des espaces de noms :

import sys
import argparse

def parse_args(parser, commands):
    # Divide argv by commands
    split_argv = [[]]
    for c in sys.argv[1:]:
        if c in commands.choices:
            split_argv.append([c])
        else:
            split_argv[-1].append(c)
    # Initialize namespace
    args = argparse.Namespace()
    for c in commands.choices:
        setattr(args, c, None)
    # Parse each command
    parser.parse_args(split_argv[0], namespace=args)  # Without command
    for argv in split_argv[1:]:  # Commands
        n = argparse.Namespace()
        setattr(args, argv[0], n)
        parser.parse_args(argv, namespace=n)
    return args

parser = argparse.ArgumentParser()
commands = parser.add_subparsers(title='sub-commands')

cmd1_parser = commands.add_parser('cmd1')
cmd1_parser.add_argument('--foo')

cmd2_parser = commands.add_parser('cmd2')
cmd2_parser.add_argument('--foo')

cmd2_parser = commands.add_parser('cmd3')
cmd2_parser.add_argument('--foo')

args = parse_args(parser, commands)
print(args)

Il se comporte correctement, en fournissant une aide argparse agréable :

Para ./test.py --help :

usage: test.py [-h] {cmd1,cmd2,cmd3} ...

optional arguments:
  -h, --help        show this help message and exit

sub-commands:
  {cmd1,cmd2,cmd3}

Para ./test.py cmd1 --help :

usage: test.py cmd1 [-h] [--foo FOO]

optional arguments:
  -h, --help  show this help message and exit
  --foo FOO

et crée une hiérarchie d'espaces de noms contenant les valeurs des arguments :

./test.py cmd1 --foo 3 cmd3 --foo 4
Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X