Estoy implementando un programa de línea de comandos que tiene una interfaz como esta:
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
He revisado la documentación de argparse . Puedo implementar GLOBAL_OPTIONScomo argumento opcional usando add_argumentin argparse. Y el {command [COMMAND_OPTS]}uso de subcomandos .
Según la documentación, parece que solo puedo tener un subcomando. Pero como puede ver, tengo que implementar uno o más subcomandos. ¿Cuál es la mejor manera de analizar el uso de argumentos de línea de comando argparse?

./setup.pytambién tiene este estilo de interfaz CLI, sería interesante ver su código fuente.Respuestas:
Se me ocurrió la misma pregunta, y parece que tengo una mejor respuesta.
La solución es que no simplemente anidaremos subparser con otro subparser, sino que podemos agregar subparser siguiendo con un parser siguiendo a 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()fuente
argparsepermite subanálisis anidados. Pero solo los he visto usados en otro lugar: en un caso de prueba para un problema de Python, bugs.python.org/issue14365@mgilson tiene una buena respuesta a esta pregunta. Pero el problema de dividir sys.argv es que pierdo todo el 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-parsersAhora, después del primer análisis, todos los comandos encadenados se almacenan en formato
extra. Lo analizo mientras no está vacío para obtener todos los comandos encadenados y crear espacios de nombres separados para ellos. Y obtengo una cadena de uso más agradable que genera argparse.fuente
namespaceel analizador sintáctico llamandonamespace = argparser.parse_args(), llamoparse_extraconparserynamespace.extra_namespaces = parse_extra( argparser, namespace )parseren el código que tienes? Solo veo que se usa para agregar elextraargumento. Luego lo mencionaste nuevamente en el comentario anterior. ¿Se supone que debe serargparser?argparser. Lo editará.parser_b = subparsers.add_parser('command_b', help='command_b help'):;parser_b.add_argument('--baz', choices='XYZ', help='baz help');options = argparser.parse_args(['--foo', 'command_a', 'command_b', '--baz', 'Z']); Esto falla con un errorPROG: error: unrecognized arguments: --baz Z. La razón es que durante el análisis decommand_a, los argumentos opcionales decommand_bya se han analizado (y son desconocidos para el sub analizador decommand_a).parse_known_argsdevuelve un espacio de nombres y una lista de cadenas desconocidas. Esto es similar a laextrarespuesta 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, restproduce:
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 ciclo alternativo le daría a cada sub analizador 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)fuente
rest = '--foo 0 cmd2 --foo2 2 --bar cmd3 --foo3 3 cmd1 --foo1 1'.split()), entonces argparse terminará en enerror: too few argumentslugar de señalar la opción no válida. Esto se debe a que se dejará la opción incorrectaresthasta que no tengamos argumentos de comando.# or sys.argvdebería ser# or sys.argv[1:].Siempre puede dividir la línea de comandos usted mismo (dividir
sys.argven los nombres de sus comandos), y luego solo pasar la parte correspondiente al comando en particular aparse_args- Incluso puede usar el mismoNamespaceusando 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
fuente
itertools.groupby()! Así es como hice lo mismo antes de darme cuentagroupby().Mejorando la respuesta de @mgilson, escribí un pequeño método de análisis que divide argv en partes y coloca valores de argumentos de comandos en la 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 una 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 FOOY crea una jerarquía de espacios de nombres que contienen los valores de los argumentos:
./test.py cmd1 --foo 3 cmd3 --foo 4 Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))fuente
split_argv[0]que en realidad está vacíosplit_argv, porque agrega[c]asplit_argv(inicialmente establecido en[[]]). Si cambia la línea 7 asplit_argv = [], todo funciona como se esperaba.subparserse usó agregando dest aladd_subparsersmétodo stackoverflow.com/questions/8250010/…La solución proporcionada por @Vikas falla para argumentos opcionales específicos de 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: breakEsto usa en
parse_known_argslugar deparse_args.parse_argsaborta tan pronto como se encuentra un argumento desconocido para el sub analizador actual,parse_known_argsdevuelve como un segundo valor en la tupla devuelta. En este enfoque, los argumentos restantes se alimentan nuevamente al analizador. Entonces, 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 opciones, no a los espacios de nombre posteriores.
Este enfoque funciona bien para la mayoría de las situaciones, pero tiene tres limitaciones importantes:
myprog.py command_a --foo=bar command_b --foo=bar.nargs='?'onargs='+'onargs='*').PROG --foo command_b command_a --baz Z 12con el código anterior,--baz Zserá consumido porcommand_b, no porcommand_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 aumentará 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 esecommand_aes el valor delspamargumento opcional . En particular, al 'dividir' los argumentos opcionales y posicionales,_parse_known_args()no se fija en los nombres de los argumentos (comocommand_aocommand_b), sino simplemente en el lugar donde aparecen en la lista de argumentos. También asume que cualquier subcomando consumirá todos los argumentos restantes. Esta limitaciónargparsetambién impide una implementación adecuada de subanálisis de múltiples comandos. Desafortunadamente, esto significa que una implementación adecuada requiere una reescritura completa delargparse.ArgParser._parse_known_args()método, que son más de 200 líneas de código.Dadas estas limitaciones, puede ser una opción simplemente volver a un único 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
fuente
Podrías probar arghandler . Esta es una extensión de argparse con soporte explícito para subcomandos.
fuente
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: soy el autor. Requiere Python 3.6. Para instalar use:
Aquí está la documentación y aquí está el repositorio en GitHub .
fuente
Se construyó un completo ejemplo de Python 2/3 con subparsers ,
parse_known_argsyparse_args( que se ejecuta en Ideone ):from __future__ import print_function from argparse import ArgumentParser from random import randint def main(): parser = get_parser() input_sum_cmd = ['sum_cmd', '--sum'] input_min_cmd = ['min_cmd', '--min'] args, rest = parser.parse_known_args( # `sum` input_sum_cmd + ['-a', str(randint(21, 30)), '-b', str(randint(51, 80))] + # `min` input_min_cmd + ['-y', str(float(randint(64, 79))), '-z', str(float(randint(91, 120)) + .5)] ) print('args:\t ', args, '\nrest:\t ', rest, '\n', sep='') sum_cmd_result = args.sm((args.a, args.b)) print( 'a:\t\t {:02d}\n'.format(args.a), 'b:\t\t {:02d}\n'.format(args.b), 'sum_cmd: {:02d}\n'.format(sum_cmd_result), sep='') assert rest[0] == 'min_cmd' args = parser.parse_args(rest) min_cmd_result = args.mn((args.y, args.z)) print( 'y:\t\t {:05.2f}\n'.format(args.y), 'z:\t\t {:05.2f}\n'.format(args.z), 'min_cmd: {:05.2f}'.format(min_cmd_result), sep='') def get_parser(): # create the top-level parser parser = ArgumentParser(prog='PROG') subparsers = parser.add_subparsers(help='sub-command help') # create the parser for the "sum" command parser_a = subparsers.add_parser('sum_cmd', help='sum some integers') parser_a.add_argument('-a', type=int, help='an integer for the accumulator') parser_a.add_argument('-b', type=int, help='an integer for the accumulator') parser_a.add_argument('--sum', dest='sm', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') # create the parser for the "min" command parser_b = subparsers.add_parser('min_cmd', help='min some integers') parser_b.add_argument('-y', type=float, help='an float for the accumulator') parser_b.add_argument('-z', type=float, help='an float for the accumulator') parser_b.add_argument('--min', dest='mn', action='store_const', const=min, default=0, help='smallest integer (default: 0)') return parser if __name__ == '__main__': main()fuente
Tenía más o menos los mismos requisitos: poder establecer argumentos globales y poder encadenar comandos y ejecutarlos en el orden de la línea de comandos .
Terminé con el siguiente código. Usé algunas partes del código de este y otros hilos.
# argtest.py import sys import argparse def init_args(): def parse_args_into_namespaces(parser, commands): ''' Split all command arguments (without prefix, like --) in own namespaces. Each command accepts extra options for configuration. Example: `add 2 mul 5 --repeat 3` could be used to a sequencial addition of 2, then multiply with 5 repeated 3 times. ''' class OrderNamespace(argparse.Namespace): ''' Add `command_order` attribute - a list of command in order on the command line. This allows sequencial processing of arguments. ''' globals = None def __init__(self, **kwargs): self.command_order = [] super(OrderNamespace, self).__init__(**kwargs) def __setattr__(self, attr, value): attr = attr.replace('-', '_') if value and attr not in self.command_order: self.command_order.append(attr) super(OrderNamespace, self).__setattr__(attr, value) # 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) # Globals arguments without commands args = OrderNamespace() cmd, args_raw = 'globals', split_argv.pop(0) args_parsed = parser.parse_args(args_raw, namespace=OrderNamespace()) setattr(args, cmd, args_parsed) # Split all commands to separate namespace pos = 0 while len(split_argv): pos += 1 cmd, *args_raw = split_argv.pop(0) assert cmd[0].isalpha(), 'Command must start with a letter.' args_parsed = commands.choices[cmd].parse_args(args_raw, namespace=OrderNamespace()) setattr(args, f'{cmd}~{pos}', args_parsed) return args # # Supported commands and options # parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--print', action='store_true') commands = parser.add_subparsers(title='Operation chain') cmd1_parser = commands.add_parser('add', formatter_class=argparse.ArgumentDefaultsHelpFormatter) cmd1_parser.add_argument('add', help='Add this number.', type=float) cmd1_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.', default=1, type=int) cmd2_parser = commands.add_parser('mult', formatter_class=argparse.ArgumentDefaultsHelpFormatter) cmd2_parser.add_argument('mult', help='Multiply with this number.', type=float) cmd2_parser.add_argument('-r', '--repeat', help='Repeat this operation N times.', default=1, type=int) args = parse_args_into_namespaces(parser, commands) return args # # DEMO # args = init_args() # print('Parsed arguments:') # for cmd in args.command_order: # namespace = getattr(args, cmd) # for option_name in namespace.command_order: # option_value = getattr(namespace, option_name) # print((cmd, option_name, option_value)) print('Execution:') result = 0 for cmd in args.command_order: namespace = getattr(args, cmd) cmd_name, cmd_position = cmd.split('~') if cmd.find('~') > -1 else (cmd, 0) if cmd_name == 'globals': pass elif cmd_name == 'add': for r in range(namespace.repeat): if args.globals.print: print(f'+ {namespace.add}') result = result + namespace.add elif cmd_name == 'mult': for r in range(namespace.repeat): if args.globals.print: print(f'* {namespace.mult}') result = result * namespace.mult else: raise NotImplementedError(f'Namespace `{cmd}` is not implemented.') print(10*'-') print(result)A continuación un ejemplo:
$ python argstest.py --print add 1 -r 2 mult 5 add 3 mult -r 5 5 Execution: + 1.0 + 1.0 * 5.0 + 3.0 * 5.0 * 5.0 * 5.0 * 5.0 * 5.0 ---------- 40625.0fuente
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.alphafuente