python argparseを使用して複数のネストされたサブコマンドを解析する方法は?


82

私は次のようなインターフェイスを持つコマンドラインプログラムを実装しています:

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

私が通過したargparseドキュメント。をGLOBAL_OPTIONS使用add_argumentしてオプションの引数として実装できますargparse。そして、サブコマンド{command [COMMAND_OPTS]}を使用します

ドキュメントから、サブコマンドは1つしか持てないようです。しかし、ご覧のとおり、1つ以上のサブコマンドを実装する必要があります。を使用してそのようなコマンドライン引数を解析するための最良の方法は何argparseですか?


2
これがサブコマンドの目的ではないと思います。ドキュメントから、これは本質的に、個別の個別のサブプログラムを制御するためのものであると記載されています。引数グループを調べましたか?
クリス

distutilsに./setup.pyもこのスタイルのCLIインターフェイスがあり、ソースコードを調べると興味深いでしょう。
CiroSantilli郝海东冠事病六四事件法轮功

回答:


27

私は同じ質問を思いついた、そして私はより良い答えを持っているようだ。

解決策は、サブパーサーを別のサブパーサーと単純にネストするのではなく、別のサブパーサーの後にパーサーを追加してサブパーサーを追加することです。

コードはその方法を教えてくれます:

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

はい、argparseネストされたサブパーサーは許可されます。しかし、私はそれらが他の1つの場所で使用されているのを見ただけです-Pythonの
hpaulj

9
これは、コマンドがネストされた構造を持っていることを前提としています。しかし、問題は「並列」コマンドを要求することです
augurar 2014

25

@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
私が取得した後@Flaviusは、namespace呼び出すことによって、パーサからnamespace = argparser.parse_args()、私は呼んparse_extraparsernamespaceextra_namespaces = parse_extra( argparser, namespace )
Vikas 2013年

私は論理を理解していると思いparserますが、あなたが持っているコードには何が含まれていますか。extra引数を追加するために使用されているのがわかります。それからあなたは上のコメントでそれを再び言及しました。あるはずargparserですか?
jmlopez 2013

@jmlopezええそれはあるべきですargparser。編集します。
Vikas

1
このソリューションは、サブコマンド固有のオプションの引数では失敗することに注意してください。別の解決策については、以下の私の解決策(stackoverflow.com/a/49977713/428542)を参照してください。
MacFreek 2018

1
これがどのように失敗するかの例を次に示します。次の3行を追加しますparser_b = subparsers.add_parser('command_b', help='command_b help')。; parser_b.add_argument('--baz', choices='XYZ', help='baz help'); options = argparser.parse_args(['--foo', 'command_a', 'command_b', '--baz', 'Z']); これはエラーで失敗しますPROG: error: unrecognized arguments: --baz Z。その理由は、の解析中にcommand_a、のオプションの引数command_bがすでに解析されているためです(およびのサブパーサーでは不明ですcommand_a)。
MacFreek 2018

14

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)

うまく機能します。ただし、欠陥があります。どこかにスペルミスのあるオプションがある場合(たとえばrest = '--foo 0 cmd2 --foo2 2 --bar cmd3 --foo3 3 cmd1 --foo1 1'.split())、argparseはerror: too few arguments無効なオプションを指摘する代わりに終了します。これはrest、コマンド引数がなくなるまで、不正なオプションが残っているためです。
エイドリアン

コメント# or sys.argvはです# or sys.argv[1:]
エイドリアン

5

いつでも自分でコマンドラインを分割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...

テストされていない


1
mgilsonに感謝します。これは私の質問に対する素晴らしい解決策ですが、私はそれを少し違ったやり方でやることになりました。別の答えを追加しました。
Vikas 2012年

1
の良い使い方itertools.groupby()これは私が知る前に私が同じことをした方法groupby()です。
kzyapkov 2016年

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

上記のコードを確認すると、1つの問題が発生しました。18行目split_argv[0]では、にsplit_argv追加[c]するためsplit_argv(最初はに設定されている[[]])、で実際に空になっているものを参照します。7行目をに変更するとsplit_argv = []、すべてが期待どおりに機能します。
HEADLESS_0NE 2017年

2
私はあなたが共有したコードに(再び)いくつかの修正を行い(私が遭遇した
HEADLESS_0NE

この回答はかなりまともです、あなたはどの判断できsubparserにDESTを追加することによって、使用されたadd_subparsers方法のstackoverflow.com/questions/8250010/...
wizebin

5

@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_argsparse_args現在のサブパーサーに不明な引数が検出されるとすぐに中止されます。parse_known_argsれ、返されたタプルの2番目の値として返されます。このアプローチでは、残りの引数が再びパーサーに送られます。したがって、コマンドごとに、新しい名前空間が作成されます。

この基本的な例では、すべてのグローバルオプションが最初のオプションの名前空間にのみ追加され、後続の名前空間には追加されないことに注意してください。

このアプローチはほとんどの状況で問題なく機能しますが、3つの重要な制限があります。

  • 次のように、異なるサブコマンドに同じオプションの引数を使用することはできません。 myprog.py command_a --foo=bar command_b --foo=bar
  • サブコマンド(nargs='?'または)で可変長の位置引数を使用することはできません。nargs='+'またはnargs='*')。
  • 既知の引数はすべて、新しいコマンドで「中断」することなく解析されます。たとえばPROG --foo command_b command_a --baz Z 12、上記のコードで--baz Zcommand_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オプションの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'])

使用情報にさまざまなコマンドをリストすることも可能です。私の回答を参照してくださいhttps://stackoverflow.com/a/49999185/428542


4

arghandlerを試すことができます。これは、サブコマンドを明示的にサポートするargparseの拡張機能です。


3
arghandlerは、サブコマンドを宣言するための優れた方法を提供します。ただし、これがOPの質問(複数のサブコマンドの解析)の解決にどのように役立つかはわかりません。解析された最初のサブコマンドは残りのすべての引数を使い果たすため、それ以降のコマンドは解析されません。arghandlerでこれを解決する方法のヒントを教えてください。ありがとう。
エイドリアン

1

並列パーサーをサポートする別のパッケージは「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
)

免責事項:私は著者です。Python3.6が必要です。使用するには:

pip3 install declarative_parser

これがドキュメントで、これがGitHubリポジトリです。


1

完全なPython 2/3例を建てsubparsersparse_known_argsおよびparse_argsIDEone上で実行されています):

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

0

私には多かれ少なかれ同じ要件がありました:グローバル引数を設定でき、コマンドをチェーンしてコマンドラインの順序で実行できること。

私は次のコードに行き着きました。このスレッドや他のスレッドのコードの一部を使用しました。

# 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

-4

パッケージ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

1
これは実際には質問に答えません。また、optparseは非推奨になりました(python docs「optparseモジュールは非推奨であり、これ以上開発されません。開発はargparseモジュールで続行されます」から)。
クリス

反対票を投じて申し訳ありませんが、これは私が尋ねた質問に対応していません。
Vikas 2012年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.