Analice múltiples subcomandos en Python simultáneamente u otra forma de agrupar argumentos analizados

Estoy convirtiendo la utilidad del instalador de shell Bash a Python 2.7 y necesito implementar una CLI compleja, por lo que puedo analizar decenas de parámetros (potencialmente hasta ~ 150). Estos son nombres de variables de clase Puppet además de una docena de opciones de implementación genéricas, que estaban disponibles en la versión de shell.

Sin embargo, después de que comencé a agregar más variables, enfrenté varios desafíos: 1. Necesito agrupar parámetros en diccionarios separados para que las opciones de implementación estén separadas de las variables Puppet. Si se lanzan en el mismo cubo, tendré que escribir un poco de lógica para clasificarlos, potencialmente cambiar el nombre de los parámetros y luego las combinaciones de diccionarios no serán triviales. 2. Es posible que existan variables con el mismo nombre pero que pertenezcan a diferentes clases de Puppet, por lo que pensé que los subcomandos me permitirían filtrar lo que va a dónde y evitar colisiones de nombres.

En el momento en que implementé el análisis de parámetros mediante la simple adición de varios analizadores:

parser = argparse.ArgumentParser(description='deployment parameters.') env_select = parser.add_argument_group(None, 'Environment selection') env_select.add_argument('-c', '--client_id', help='Client name to use.') env_select.add_argument('-e', '--environment', help='Environment name to use.') setup_type = parser.add_argument_group(None, 'What kind of setup should be done:') setup_type.add_argument('-i', '--install', choices=ANSWERS, metavar='', action=StoreBool, help='Yy/Nn Do normal install and configuration') # MORE setup options ... args, unk = parser.parse_known_args() config['deploy_cfg'].update(args.__dict__) pup_class1_parser = argparse.ArgumentParser(description=None) pup_class1 = pup_class1_parser.add_argument_group(None, 'Puppet variables') pup_class1.add_argument('--ad_domain', help='AD/LDAP domain name.') pup_class1.add_argument('--ad_host', help='AD/LDAP server name.') # Rest of the parameters args, unk = pup_class1_parser.parse_known_args() config['pup_class1'] = dict({}) config['pup_class1'].update(args.__dict__) # Same for class2, class3 and so on. 

El problema con este enfoque es que no resuelve el problema 2. También el primer analizador consume la opción “-h” y el rest de parámetros no se muestran en la ayuda.

Intenté usar el ejemplo seleccionado como respuesta, pero no pude usar ambos comandos a la vez.

     ## 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 pp = pprint.PrettyPrinter(indent=4) 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 parser_a.add_argument('--opt_a1', help='option a1') parser_a.add_argument('--opt_a2', help='option a2') parser_b = subparsers.add_parser('command_b', help = "command_b help") ## Setup options for parser_a parser_b.add_argument('--opt_b1', help='option b1') parser_b.add_argument('--opt_b2', help='option b2') ## Add nargs="*" for zero or more other commands argparser.add_argument('extra', nargs = "*", help = 'Other commands') namespace = argparser.parse_args() pp.pprint(namespace) extra_namespaces = parse_extra( argparser, namespace ) pp.pprint(extra_namespaces) 

    Resultados me en:

     $ python argtest.py command_b --opt_b1 b1 --opt_b2 b2 command_a --opt_a1 a1 usage: argtest.py [-h] {command_a,command_b} ... [extra [extra ...]] argtest.py: error: unrecognized arguments: command_a --opt_a1 a1 

    El mismo resultado fue cuando traté de definir padre con dos analizadores hijos.

    Preguntas

    1. ¿Puedo usar parser.add_argument_group para el análisis de argumentos o es solo para la agrupación en la ayuda de impresión? Solucionaría el problema 1 sin perder el efecto secundario de la ayuda. Al pasarlo como parse_known_args(namespace=argument_group) (si recuerdo correctamente mis experimentos) obtiene todas las variables (eso es correcto) pero también obtiene todas las cosas del objeto Python en el dictado resultante (eso es malo para el YAML hieradata)
    2. ¿Qué me falta en el segundo ejemplo para permitir el uso de varios subcomandos? ¿O es eso imposible con argparse?
    3. ¿Alguna otra sugerencia para agrupar variables de línea de comando? He mirado a Click, pero no encontré ninguna ventaja sobre el argparse estándar para mi tarea.

    Nota: soy administrador del sistema, no soy un progtwigdor, así que sea amable conmigo para la encoding de estilo sin objeto. 🙂

    Gracias

    RESUELTO La agrupación de argumentos se resolvió mediante la respuesta sugerida por hpaulj .

     import argparse import pprint parser = argparse.ArgumentParser() group_list = ['group1', 'group2'] group1 = parser.add_argument_group('group1') group1.add_argument('--test11', help="test11") group1.add_argument('--test12', help="test12") group2 = parser.add_argument_group('group2') group2.add_argument('--test21', help="test21") group2.add_argument('--test22', help="test22") args = parser.parse_args() pp = pprint.PrettyPrinter(indent=4) d = dict({}) for group in parser._action_groups: if group.title in group_list: d[group.title]={a.dest:getattr(args,a.dest,None) for a in group._group_actions} print "Parsed arguments" pp.pprint(d) 

    Esto me da el resultado deseado para el problema No.1. Hasta que tenga múltiples parámetros con el mismo nombre. La solución puede parecer fea, pero al menos funciona como se espera.

     python argtest4.py --test22 aa --test11 yy11 --test21 aaa21 Parsed arguments { 'group1': { 'test11': 'yy11', 'test12': None}, 'group2': { 'test21': 'aaa21', 'test22': 'aa'}} 

    Su pregunta es demasiado complicada de entender y responder en un bash. Pero voy a tirar algunas ideas preliminares.

    Sí, los grupos de argument_groups son solo una forma de agrupar argumentos en la ayuda. No tienen efecto en el análisis.

    Otro SO reciente preguntó sobre el análisis de grupos de argumentos:

    ¿Es posible analizar solo los parámetros de un grupo de argumentos con argparse?

    Ese cartel inicialmente quería usar un grupo como analizador, pero la estructura de clase argparse no lo permite. argparse está escrito en estilo de objeto. parser=ArguementParser... crea una clase de objeto, parser.add_arguement... crea otra, add_argument_group... y otra más. Se personaliza subclasificando las clases ArgumentParser o HelpFormatter o Action , etc.

    Mencioné un mecanismo de parents . Usted define uno o más analizadores parentales, y los utiliza para rellenar su analizador “principal”. Se podrían ejecutar de forma independiente (con parse_known_args), mientras que ‘main’ se utiliza para manejar la ayuda.

    También discutimos agrupar los argumentos después de analizar. Un namespace es un objeto simple, en el que cada argumento es un atributo. También se puede convertir en un diccionario. Es fácil extraer grupos de elementos de un diccionario.

    Hay preguntas de SO sobre el uso de subparsers múltiples. Esa es una proposición incómoda. Posible, pero no es fácil. Los subparadores son como emitir un comando a un progtwig del sistema. Generalmente se emite un comando por llamada. No los anidas ni emites secuencias. Dejas que el shell piping y los scripts manejen múltiples acciones.

    IPython usa argparse para analizar sus entradas. Atrapa la ayuda primero y emite su propio mensaje. La mayoría de los argumentos provienen de archivos de configuración, por lo que es posible establecer valores con configuraciones predeterminadas, configuraciones personalizadas y en la línea de comandos. Es un ejemplo de nombrar un conjunto muy grande de argumentos.

    Los subparsers le permiten usar el mismo nombre de argumento, pero sin poder invocar múltiples subparsers en una llamada, eso no ayuda mucho. E incluso si pudiera invocar varios subparsers, todavía pondrían los argumentos en el mismo espacio de nombres. También argparse trata de manejar los argumentos flage de una manera independiente de orden. Así que un --foo al final de la línea de comando se analiza de la misma manera que si estuviera al principio.

    Hubo una pregunta SO en la que discutimos el uso de nombres de argumento (‘dest’) como 'group1.argument1' , e incluso discutí el uso de espacios de nombres nesteds. Podría mirar eso si ayudaría.


    Otro pensamiento: cargar sys.argv y particionarlo antes de pasarlo a uno o más analizadores. Podrías dividirlo en alguna palabra clave, o en prefijos, etc.

    Si tiene tantos argumentos, esto parece ser un problema de diseño. Parece muy inmanejable. ¿No puede implementar esto utilizando un archivo de configuración que tiene un conjunto razonable de valores predeterminados? ¿O los valores predeterminados en el código con un número razonable (es decir, PEQUEÑO) de argumentos en la línea de comandos, y permiten que todo o todo lo demás se anule con los parámetros en un archivo de configuración ‘clave: valor’? No puedo imaginar tener que usar un CLI con la cantidad de variables que está proponiendo.