web-dev-qa-db-fra.com

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

J'implémente un programme en ligne de commande qui a une interface comme celle-ci:

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

J'ai parcouru la documentation argparse . Je peux implémenter GLOBAL_OPTIONS comme argument facultatif en utilisant add_argument dans 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?

63
Vikas

@mgilson a un bon réponse à cette question. Mais le problème avec le fractionnement de sys.argv moi-même est que je perds tout le message d'aide Nice généré par Argparse pour l'utilisateur. Alors j'ai fini par faire ça:

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

Maintenant, après la première analyse, toutes les commandes chaînées sont stockées dans extra. Je le réanalyse alors qu'il n'est pas vide pour obtenir toutes les commandes chaînées et créer des espaces de noms séparés pour elles. Et j'obtiens une chaîne d'utilisation plus agréable générée par argparse.

22
Vikas

Je suis venu avec la même qustion, et il semble que j'ai une meilleure réponse.

La solution est de ne pas simplement imbriquer un sous-analyseur avec un autre analyseur, mais nous pouvons ajouter un analyseur suivant avec un analyseur suivant un autre analyseur.

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()   
15
Xiongjun Liang

parse_known_args renvoie un espace de noms et une liste de chaînes inconnues. Ceci est similaire au 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 boucle alternative donnerait à chaque sous-analyseur son propre espace de noms. Cela permet le chevauchement des noms de position.

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

Vous pouvez essayer arghandler . Il s'agit d'une extension de argparse avec un support explicite pour les sous-commandes.

4
Derek Ruths

Vous pouvez toujours diviser la ligne de commande vous-même (split sys.argv sur vos noms de commande), puis passez uniquement la partie correspondant à la commande particulière à parse_args - Vous pouvez même utiliser le même Namespace en utilisant le mot-clé namespace si vous le souhaitez.

Le regroupement de la ligne de commande est facile avec itertools.groupby:

import sys
import itertools
import argparse    

mycommands=['cmd1','cmd2','cmd3']

def groupargs(arg,currentarg=[None]):
    if(arg in mycommands):currentarg[0]=arg
    return currentarg[0]

commandlines=[list(args) for cmd,args in intertools.groupby(sys.argv,groupargs)]

#setup parser here...
parser=argparse.ArgumentParser()
#...

namespace=argparse.Namespace()
for cmdline in commandlines:
    parser.parse_args(cmdline,namespace=namespace)

#Now do something with namespace...

non testé

4
mgilson

Un autre paquet qui prend en charge les analyseurs parallèles est "declarative_parser".

import argparse
from declarative_parser import Parser, Argument

supported_formats = ['png', 'jpeg', 'gif']

class InputParser(Parser):
    path = Argument(type=argparse.FileType('rb'), optional=False)
    format = Argument(default='png', choices=supported_formats)

class OutputParser(Parser):
    format = Argument(default='jpeg', choices=supported_formats)

class ImageConverter(Parser):
    description = 'This app converts images'

    verbose = Argument(action='store_true')
    input = InputParser()
    output = OutputParser()

parser = ImageConverter()

commands = '--verbose input image.jpeg --format jpeg output --format gif'.split()

namespace = parser.parse_args(commands)

et l'espace de noms devient:

Namespace(
    input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>),
    output=Namespace(format='gif'),
    verbose=True
)

Avertissement: je suis l'auteur. Nécessite Python 3.6. Pour installer, utilisez:

pip3 install declarative_parser

Voici le documentation et voici le repo sur GitHub .

1
krassowski