Подтвердить что ты не робот

Как анализировать несколько вложенных подкоманд с помощью python argparse?

Я реализую программу командной строки с таким интерфейсом:

cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]

Я просмотрел документацию argparse. Я могу реализовать GLOBAL_OPTIONS как необязательный аргумент, используя add_argument в argparse. И {command [COMMAND_OPTS]}, используя Sub-commands.

Из документации кажется, что у меня может быть только одна подкоманда. Но, как вы видите, мне нужно реализовать одну или несколько подкоманд. Каков наилучший способ проанализировать такие аргументы командной строки, используя argparse?

4b9b3361

Ответ 1

@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.

Ответ 2

Я придумал то же самое, и, похоже, у меня есть лучший ответ.

Решение состоит в том, что мы не будем просто вставлять 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()   

Ответ 3

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)

Ответ 4

Вы всегда можете разделить командную строку самостоятельно (разделить 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...

непроверенных

Ответ 5

Улучшая ответ на @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'))

Ответ 6

Вы можете попробовать arghandler. Это расширение для argparse с явной поддержкой подкоманд.

Ответ 7

Другим пакетом, который поддерживает параллельные парсеры, является "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

Вот документация, и вот repo на GitHub.

Ответ 8

Решение, предоставляемое @Vikas, не подходит для subcommand- конкретных необязательных аргументов, но подход действителен. Вот улучшенная версия:

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 при использовании одного subcommand-:

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'])

Можно даже перечислить различные команды в информации об использовании, см. Мой ответ fooobar.com/questions/15555369/...

Ответ 9

вы можете использовать пакет 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