store_true - python argparse argumentparser()
¿Cómo analizar varios subcomandos anidados usando python argparse? (9)
@mgilson tiene una buena answer a esta pregunta. Pero el problema con la división de 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 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.
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 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-commands .
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
?
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 de nuevo 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=''?''
Onargs=''+''
onargs=''*''
). - 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 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 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://.com/a/49999185/428542
Mejorando la respuesta con @mgilson, escribí un pequeño método de análisis que divide el argumento argv en partes y coloca 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''))
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 documentation y aquí está el repositorio 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
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()
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
Usted podría intentar arghandler . Esta es una extensión para argparse con soporte explícito para subcomandos.
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)