私はこのようなインターフェースを持つコマンドラインプログラムを実装しています:
cmd [GLOBAL_OPTIONS] {command [COMMAND_OPTS]} [{command [COMMAND_OPTS]} ...]
argparse documentation を実行しました。 argparse
のGLOBAL_OPTIONS
を使用して、add_argument
をオプションの引数として実装できます。そして、 サブコマンド を使用した{command [COMMAND_OPTS]}
。
ドキュメントから、サブコマンドは1つしか持てないようです。しかし、ご覧のとおり、1つ以上のサブコマンドを実装する必要があります。 argparse
を使用してこのようなコマンドライン引数を解析する最良の方法は何ですか?
@mgilsonには、この質問に対するニース answer があります。しかし、sys.argvを自分で分割する際の問題は、Argparseがユーザーに対して生成するすべてのNiceヘルプメッセージを失うことです。だから私はこれをやった:
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)
arghandler を試すことができます。これは、サブコマンドを明示的にサポートするargparseの拡張です。
いつでも自分でコマンドラインを分割し(コマンド名で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...
未テスト
@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)
Nice 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'))
@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
_は返されたTupleの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 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
_がオプションの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
並列パーサーをサポートする別のパッケージは「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