Как разобрать несколько вложенных подкоманд с помощью python argparse?
я реализую программу командной строки, которая имеет такой интерфейс:
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
Я прошел через argparse documentation. Я могу реализовать GLOBAL_OPTIONS в качестве необязательного аргумента с помощью add_argument на argparse. А то {command [COMMAND_OPTS]} используя команды.
из документации кажется, что у меня может быть только одна суб-команда. Но, как вы можете видеть, я должен реализовать одну или несколько подкоманд. Что является лучшим способом, чтобы разобрать такую команду аргументы строки с использованием argparse?
9 ответов:
@mgilson имеет хороший ответ на этот вопрос. Но проблема с разделением sys.сам argv заключается в том, что я теряю все хорошее сообщение справки, которое Argparse генерирует для пользователя. Так что я закончил тем, что сделал это:
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теперь после первого разбора все цепные команды хранятся в
extra. Я переделываю его, пока он не пуст, чтобы получить все цепные команды и создать для них отдельные пространства имен. И я получаю более приятную строку использования, которую генерирует argparse.
Я придумал тот же вопрос, и, кажется, у меня есть лучший ответ.
решение заключается в том, что мы не будем просто вложить subparser с другим subparser, но мы можем добавить subparser следующий с синтаксическим анализатором после другого subparser.
код говорит вам, как:
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возвращает пространство имен и список неизвестных строк. Это похоже наextraв проверяемый ответ.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выдает:
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') []альтернативный цикл даст каждому подразделу свое собственное пространство имен. Это позволяет перекрывать имена позиционеров.
argslist = [] while rest: args,rest = parser.parse_known_args(rest) argslist.append(args)
вы всегда можете разделить командную строку самостоятельно (split
sys.argvпо именам команд), а затем только передать часть, соответствующую конкретной команде вparse_args- вы даже можете использовать тот жеNamespaceС помощью ключевого слова namespace, если вы хотите.группировка командной строки легко
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...непроверенные
улучшая ответ @mgilson, я написал небольшой метод парсинга, который разбивает argv на части и помещает значения аргументов команд в иерархию пространств имен:
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)он ведет себя правильно, обеспечивая хорошую помощь argparse:
на
./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}на
./test.py cmd1 --help:usage: test.py cmd1 [-h] [--foo FOO] optional arguments: -h, --help show this help message and exit --foo FOOи создает иерархию пространств имен, содержащих значения аргументов:
./test.py cmd1 --foo 3 cmd3 --foo 4 Namespace(cmd1=Namespace(foo='3'), cmd2=None, cmd3=Namespace(foo='4'))
вы могли бы попробовать arghandler. Это расширение для argparse с явной поддержкой подкоманд.
другой пакет, который поддерживает параллельные Парсеры-это "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)и пространство имен становится:
Namespace( input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>), output=Namespace(format='gif'), verbose=True )отказ от ответственности: я автор. Требуется Python 3.6. Для установки используйте:
pip3 install declarative_parserздесь документация и вот это РЕПО на GitHub.
решение, предоставленное @Vikas, не подходит для необязательных аргументов, специфичных для подкоманды, но подход допустим. Вот улучшенная версия:
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использует
parse_known_argsвместоparse_args.parse_argsпрерывается, как только встречается аргумент, неизвестный текущему подразделу,parse_known_argsвозвращает их как второе значение в возвращаемом кортеже. При таком подходе оставшиеся аргументы снова подаются в синтаксический анализатор. Поэтому для каждой команды создается новое пространство имен создан.обратите внимание, что в этом базовом примере все глобальные параметры добавляются только в первое пространство имен options, а не в последующие пространства имен.
этот подход прекрасно работает для большинства ситуаций, но имеет три важных ограничения:
- невозможно использовать один и тот же необязательный аргумент для разных подкоманд, например
myprog.py command_a --foo=bar command_b --foo=bar.- невозможно использовать любые позиционные аргументы переменной длины с подкомандами (
nargs='?'илиnargs='+'илиnargs='*').- любой известный аргумент анализируется без "взлома" при новой команде. Например, в
PROG --foo command_b command_a --baz Z 12с вышеуказанным кодом,--baz Zбудет потребленоcommand_b, а неcommand_a.эти ограничения являются прямым ограничением argparse. Вот простой пример, который показывает ограничения argparse-даже при использовании одной подкоманды -:
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)это поднимет
error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b').причина в том, что внутренний метод
argparse.ArgParser._parse_known_args()он слишком жаден и предполагает, чтоcommand_a- это значение необязательного
вы можете использовать пакет 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
Comments