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_OPTIONS
como argumento opcional usando add_argument
in 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.py
tambié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
argparse
permite 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-parsers
Ahora, 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
namespace
el analizador sintáctico llamandonamespace = argparser.parse_args()
, llamoparse_extra
conparser
ynamespace
.extra_namespaces = parse_extra( argparser, namespace )
parser
en el código que tienes? Solo veo que se usa para agregar elextra
argumento. 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_b
ya se han analizado (y son desconocidos para el sub analizador decommand_a
).parse_known_args
devuelve un espacio de nombres y una lista de cadenas desconocidas. Esto es similar a laextra
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 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 arguments
lugar de señalar la opción no válida. Esto se debe a que se dejará la opción incorrectarest
hasta que no tengamos argumentos de comando.# or sys.argv
debería ser# or sys.argv[1:]
.Siempre puede dividir la línea de comandos usted mismo (dividir
sys.argv
en los nombres de sus comandos), y luego solo pasar la parte correspondiente al comando en particular aparse_args
- Incluso puede usar el mismoNamespace
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
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 FOO
Y 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.subparser
se usó agregando dest aladd_subparsers
mé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: break
Esto usa en
parse_known_args
lugar deparse_args
.parse_args
aborta tan pronto como se encuentra un argumento desconocido para el sub analizador actual,parse_known_args
devuelve 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 12
con el código anterior,--baz Z
será 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_a
es el valor delspam
argumento opcional . En particular, al 'dividir' los argumentos opcionales y posicionales,_parse_known_args()
no se fija en los nombres de los argumentos (comocommand_a
ocommand_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ónargparse
tambié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_args
yparse_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.0
fuente
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
fuente