Как проанализировать несколько вложенных подкоманд, используя python argparse?
Я реализую программу командной строки, которая имеет такой интерфейс:
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
Я просмотрел argparse документацию. Я могу реализовать GLOBAL_OPTIONS
в качестве необязательного аргумента, используя add_argument
в argparse
, И {command [COMMAND_OPTS]}
используя подкоманды.
Из документации кажется, что у меня может быть только одна подкоманда. Но, как вы можете видеть, я должен реализовать одну или несколько подкоманд. Каков наилучший способ разобрать такие аргументы командной строки, используя argparse
?
13 ответов
У @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.
Я придумал тот же вопрос, и, кажется, я получил лучший ответ.
Решение состоит в том, что мы не просто вложим подпарапер в другой подпарсер, но мы можем добавить подпаразер, следующий за парсером, следующий за другим подпарсером.
Код расскажет вам как:
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)
Решение, предоставляемое @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
возвращает их как второе значение в возвращаемом кортеже. При таком подходе оставшиеся аргументы снова передаются парсеру. Таким образом, для каждой команды создается новое пространство имен.
Обратите внимание, что в этом базовом примере все глобальные параметры добавляются только к первым пространствам имен, а не к последующим пространствам имен.
Этот подход прекрасно работает в большинстве ситуаций, но имеет три важных ограничения:
- Невозможно использовать один и тот же необязательный аргумент для разных подкоманд, например
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 -even при использовании одной подкоманды-:
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
это значение необязательного spam
аргумент. В частности, когда "разделяются" необязательные и позиционные аргументы, _parse_known_args()
не смотрит на названия аругментов (вроде command_a
или же command_b
), но только там, где они встречаются в списке аргументов. Также предполагается, что любая подкоманда будет использовать все оставшиеся аргументы. Это ограничение argparse
также предотвращает правильную реализацию многокомандных подпарсеров. К сожалению, это означает, что правильная реализация требует полного переписывания argparse.ArgParser._parse_known_args()
метод, который составляет 200+ строк кода.
Учитывая эти ограничения, могут быть варианты просто вернуться к одному аргументу с множественным выбором вместо подкоманд:
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'])
Можно даже перечислить различные команды в информации об использовании, см. Мой ответ /questions/32842267/python-argparse-spisok-otdelnyih-variantov-ispolzovaniya/32842273#32842273
Улучшая ответ @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'))
Вы всегда можете разделить командную строку самостоятельно (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...
непроверенной
Создал полный пример Python 2/3 с подпараметрами, parse_known_args
а также parse_args
( работает на 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()
Вы можете попробовать Arghandler. Это расширение для argparse с явной поддержкой подкоманд.
У меня были более или менее те же требования: возможность устанавливать глобальные аргументы и возможность связывать команды и выполнять их в порядке командной строки.
В итоге я получил следующий код. Я использовал некоторые части кода из этого и других потоков.
# 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)
Ниже пример:
$ 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
Еще один пакет, который поддерживает параллельные парсеры, это "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.
Вы можете использовать этот multicommand-arg-parser, который я написал. Его легко использовать: напишите свой шаблон в формате json и примените парсер.
https://github.com/antoniofrs/multicommand-args-parser
Например:
[
{
"id": "c1_id",
"command": "command1",
"help": "Select c1",
"args": [ ],
"subCommands": [
{
"id": "sc1_id",
"command": "subcommand1",
"help": "select sc1",
"args": [ ]
},
{
"id": "sc2_id",
"command": "subcommand2",
"help": "Select c12",
"args": [ ]
}
]
},
{
"id": "c1_id",
"command": "command2",
"help": "Selected c2",
"args": [ ]
}
]
Создает следующую структуру:
команда1 --my-args-here
команда1 подкоманда1 --my-args
-here команда1 подкоманда2 --my-args-here
команда2 --my-args-here
Гдеid
, который вы можете получить изagrs.command_id
, поможет вам получить команду, введенную пользователем.
Вы можете указать список аргументов в соответствующем поле (см. файл readme репозитория).
Помните, что подкоманды наследуют аргументы команды. (напримерsubcommand1
содержит также аргументыcommand
)
Структура json рекурсивная, поэтому вы можете создавать столько уровней подкоманд, сколько захотите.
Чтобы разобрать подкоманды, я использовал следующее (отсылка к коду argparse.py). Он анализирует аргументы вспомогательного анализатора и сохраняет справку для обоих. Ничего дополнительного там не проходило.
args, _ = parser.parse_known_args()
Вы можете использовать пакет 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