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