¿Cómo analizar varios subcomandos nesteds usando python argparse?

Estoy implementando un progtwig de línea de comandos que tiene una interfaz como esta:

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

He pasado por la documentación argparse . Puedo implementar GLOBAL_OPTIONS como argumento opcional usando add_argument en argparse . Y el {command [COMMAND_OPTS]} usando Sub-comandos .

De la documentación parece que solo puedo tener un subcomando. Pero como pueden ver, tengo que implementar uno o más subcomandos. ¿Cuál es la mejor manera de analizar los argumentos de la línea de comandos argparse ?

@mgilson tiene una buena respuesta a esta pregunta. Pero el problema con la división de sys.argv es que pierdo todo el bonito mensaje de ayuda que Argparse genera para el usuario. Así que terminé haciendo esto:

 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 

Ahora, después del primer análisis, todos los comandos encadenados se almacenan en extra . Lo repito mientras no está vacío para obtener todos los comandos encadenados y crear espacios de nombres separados para ellos. Y obtengo una mejor cadena de uso que argparse genera.

Se me ocurrió la misma qustion, y parece que tengo una respuesta mejor.

La solución es que no simplemente anidaremos subparser con otro subparser, sino que podemos agregar subparser a continuación con un analizador después de otro subparser.

El código te dice cómo:

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

parse_known_args devuelve un espacio de nombres y una lista de cadenas desconocidas. Esto es similar al extra en la respuesta marcada.

 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 

produce:

 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') [] 

Un bucle alternativo daría a cada subparser su propio espacio de nombres. Esto permite la superposición de nombres posicionales.

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

Puedes probar arghandler . Esta es una extensión para argparse con soporte explícito para subcomandos.

Siempre puede dividir la línea de comandos usted mismo (dividir sys.argv en sus nombres de comandos), y luego solo pasar la parte correspondiente al comando en particular a parse_args – Incluso puede usar el mismo Namespace usando la palabra clave del espacio de nombres si lo desea .

Agrupar la línea de comandos es fácil con 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... 

no probado

Mejorando la respuesta con @mgilson, escribí un pequeño método de análisis que divide el argumento argv en partes y pone los valores de los argumentos de los comandos en una jerarquía de espacios de nombres:

 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) 

Se comporta correctamente, proporcionando buena ayuda argparse:

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 

Y crea una jerarquía de espacios de nombres que contiene los valores de argumento:

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

La solución proporcionada por @Vikas falla para los argumentos opcionales específicos del subcomando, pero el enfoque es válido. Aquí hay una versión mejorada:

 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 

Esto usa parse_known_args lugar de parse_args . parse_args anula tan pronto como se encuentra un argumento desconocido para el subparser actual, parse_known_args devuelve como un segundo valor en la tupla devuelta. En este enfoque, los argumentos restantes se envían nuevamente al analizador. Así que para cada comando, se crea un nuevo espacio de nombres.

Tenga en cuenta que en este ejemplo básico, todas las opciones globales se agregan solo al primer espacio de nombres de las opciones, no a los espacios de nombres posteriores.

Este enfoque funciona bien para la mayoría de las situaciones, pero tiene tres limitaciones importantes:

  • No es posible usar el mismo argumento opcional para diferentes subcomandos, como myprog.py command_a --foo=bar command_b --foo=bar .
  • No es posible utilizar ningún argumento posicional de longitud variable con subcomandos ( nargs='?' O nargs='+' o nargs='*' ).
  • Cualquier argumento conocido se analiza, sin ‘romper’ en el nuevo comando. Por ejemplo, en PROG --foo command_b command_a --baz Z 12 con el código anterior, --baz Z será consumido por command_b , no por command_a .

Estas limitaciones son una limitación directa de argparse. Aquí hay un ejemplo simple que muestra las limitaciones de argparse -incluso cuando se usa un solo subcomando-:

 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) 

Esto generará el error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b') .

La causa es que el método interno argparse.ArgParser._parse_known_args() es demasiado codicioso y asume que command_a es el valor del argumento de spam opcional. En particular, cuando se ‘dividen’ los argumentos opcionales y posicionales, _parse_known_args() no mira los nombres de los argumentos (como command_a o command_b ), sino simplemente dónde aparecen en la lista de argumentos. También supone que cualquier subcomando consumirá todos los argumentos restantes. Esta limitación de argparse también impide una implementación adecuada de subparsers de comandos múltiples. Desafortunadamente, esto significa que una implementación adecuada requiere una reescritura completa del método argparse.ArgParser._parse_known_args() , que es más de 200 líneas de código.

Dada esta limitación, puede ser una opción simplemente revertir a un solo argumento de opción múltiple en lugar de subcomandos:

 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']) 

Incluso es posible enumerar los diferentes comandos en la información de uso, consulte mi respuesta https://stackoverflow.com/a/49999185/428542

Otro paquete que soporta analizadores paralelos es “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) 

y el espacio de nombres se convierte en:

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

Descargo de responsabilidad: yo soy el autor. Requiere Python 3.6. Para instalar uso:

 pip3 install declarative_parser 

Aquí está la documentación y aquí está el repository en GitHub .

Puedes usar el paquete optparse

 import optparse parser = optparse.OptionParser() parser.add_option("-f", dest="filename", help="corpus filename") parser.add_option("--alpha", dest="alpha", type="float", help="parameter alpha", default=0.5) (options, args) = parser.parse_args() fname = options.filename alpha = options.alpha