Programing

python argparse를 사용하여 여러 중첩 하위 명령을 구문 분석하는 방법은 무엇입니까?

crosscheck 2020. 11. 22. 19:01
반응형

python argparse를 사용하여 여러 중첩 하위 명령을 구문 분석하는 방법은 무엇입니까?


다음과 같은 인터페이스가있는 명령 줄 프로그램을 구현하고 있습니다.

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

나는 argparse 문서를 살펴 보았다 . in을 GLOBAL_OPTIONS사용하여 선택적 인수로 구현할 수 있습니다 . 그리고 using 하위 명령 .add_argumentargparse{command [COMMAND_OPTS]}

문서에서 하나의 하위 명령 만 가질 수있는 것 같습니다. 그러나 보시다시피 하나 이상의 하위 명령을 구현해야합니다. 사용하는 명령 줄 인수를 구문 분석하는 가장 좋은 방법은 무엇입니까 argparse?


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

언제든지 명령 줄을 직접 분할 ( sys.argv명령 이름으로 분할 ) 한 다음 특정 명령에 해당하는 부분 만 전달할 parse_args수 있습니다 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...

테스트되지 않은


arghandler 시도해 볼 수 있습니다. 이것은 부속 명령을 명시 적으로 지원하는 argparse의 확장입니다.


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

The solution provide by @Vikas fails for subcommand-specific optional arguments, but the approach is valid. Here is an improved version:

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

This uses parse_known_args instead of parse_args. parse_args aborts as soon as a argument unknown to the current subparser is encountered, parse_known_args returns them as a second value in the returned tuple. In this approach, the remaining arguments are fed again to the parser. So for each command, a new Namespace is created.

Note that in this basic example, all global options are added to the first options Namespace only, not to the subsequent Namespaces.

This approach works fine for most situations, but has three important limitations:

  • It is not possible to use the same optional argument for different subcommands, like myprog.py command_a --foo=bar command_b --foo=bar.
  • It is not possible to use any variable length positional arguments with subcommands (nargs='?' or nargs='+' or nargs='*').
  • Any known argument is parsed, without 'breaking' at the new command. E.g. in PROG --foo command_b command_a --baz Z 12 with the above code, --baz Z will be consumed by command_b, not by command_a.

These limitations are a direct limitation of argparse. Here is a simple example that shows the limitations of argparse -even when using a single 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)

This will raise the error: argument subparser_name: invalid choice: '42' (choose from 'command_a', 'command_b').

The cause is that the internal method argparse.ArgParser._parse_known_args() it is too greedy and assumes that command_a is the value of the optional spam argument. In particular, when 'splitting' up optional and positional arguments, _parse_known_args() does not look at the names of the arugments (like command_a or command_b), but merely where they occur in the argument list. It also assumes that any subcommand will consume all remaining arguments. This limitation of argparse also prevents a proper implementation of multi-command subparsers. This unfortunately means that a proper implementation requires a full rewrite of the argparse.ArgParser._parse_known_args() method, which is 200+ lines of code.

Given these limitation, it may be an options to simply revert to a single multiple-choice argument instead of subcommands:

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

It is even possible to list the different commands in the usage information, see my answer https://stackoverflow.com/a/49999185/428542


Another package which supports parallel parsers is "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)

and namespace becomes:

Namespace(
    input=Namespace(format='jpeg', path=<_io.BufferedReader name='image.jpeg'>),
    output=Namespace(format='gif'),
    verbose=True
)

Disclaimer: I am the author. Requires Python 3.6. To install use:

pip3 install declarative_parser

Here is the documentation and here is the repo on GitHub.


you can use the package 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

참고URL : https://stackoverflow.com/questions/10448200/how-to-parse-multiple-nested-sub-commands-using-python-argparse

반응형