Pythonのdocoptを使ったコマンドライン引数の処理
PythonでCLIコマンドを作成する際にオプションの処理を行いたかったので、docoptを使って実装した。
docoptとは
コマンドライン引数として処理したいオプションなどをテキストベースで記述することができる。その内容から、実行時にコマンドライン引数のパーサーの生成と、パースを行う。
docoptはもともとはPythonのライブラリだったようだが、Command-line interface description language
と謳っているだけあって、各言語の実装が展開されている。
使い方
↓こんな感じで使用する。
"""Naval Fate. Usage: naval_fate.py ship new <name>... naval_fate.py ship <name> move <x> <y> [--speed=<kn>] naval_fate.py ship shoot <x> <y> naval_fate.py mine (set|remove) <x> <y> [--moored|--drifting] naval_fate.py -h | --help naval_fate.py --version Options: -h --help Show this screen. --version Show version. --speed=<kn> Speed in knots [default: 10]. --moored Moored (anchored) mine. --drifting Drifting mine. """ from docopt import docopt if __name__ == '__main__': arguments = docopt(__doc__) print(arguments)
実行すると以下のようになる。
$ naval_fate.py ship Guardian move 10 50 --speed=20 {'--drifting': False, '--help': False, '--moored': False, '--speed': '20', '--version': False, '<name>': ['Guardian'], '<x>': '10', '<y>': '50', 'mine': False, 'move': True, 'new': False, 'remove': False, 'set': False, 'ship': True, 'shoot': False}
Usage
などの規定のキーワードとフォーマットがあり、それに従ってテキストメッセージを書いておき(docstringに書くサンプルが多い)、それを利用してオプションのパーサーとすることができる。
optparserなどコード内でパースする内容を定義するものと違って、別途ヘルプを書かなくて良いので、この仕様は個人的にはありがたい。
docopt—language for description of command-line interfaces ↑こちらで実際に試すことができる。
コマンド・サブコマンドを処理したい
git
やaws-cli
のように、サブコマンドを使用したCLIアプリを実装したかったので調べてみた。公式などいくつかのサンプルを参考に最終的には以下のようにした。
myapp/ ├── __init__.py ├── __main__.py ├── commands │ ├── __init__.py │ ├── cmd1.py │ └── cmd2.py (snip...)
myapp/__init__.py
"""Usage: myapp <command> <subcommand> [<args>...] Commands: cmd1 Run cmd1 cmd2 Run cmd2 """ def main(): args = docopt(__doc__) command_name = args.pop('<command>') if command_name == 'help': # docstringをそのまま表示できるのが良い print(__doc__) return # search module from <command> try: command = importlib.import_module('myapp.commands.'+command_name) except Exception: # import_moduleでExceptionが起きた場合にはコマンドがない扱いにする # UnknownCommandErrorは別途定義している raise UnknownCommandError(__doc__, command_name) # search function from <subcommand> subcommand_name = args.pop('<subcommand>') # commandとして読み込んだモジュールからsubcommandを取得する # subcommandは関数として定義している(後述) subcommand = getattr(command, subcommand_name, None) if subcommand: # subcommandが取得できたら、再度コマンドライン引数のパースを実行する # subcommandの内容はテンプレートになっているので、 # global_optsを渡してformatして使用する subcommand_doc = subcommand.__doc__.format(**global_opts) if len(subcommand_doc.strip()) > 0: args = docopt(subcommand_doc, version=version) else: subcommand = gen_help(command.__doc__) subcommand(global_opts, args)
myapp/commands/cmd1.py
"""Usage: {app_name} cmd1 <subcommand> [options] [<args>...] Options: --opt=<opt> Option Commands: sub1 Sub command 1 sub2 Sub command 2 """ def sub1(global_opts, args): """Usage: {app_name} cmd1 sub1 --opt1=<opt1> [--flag] [<args>...] Options: --opt1=<opt1> Option --flag flag """ # ↑app_nameはdocoptに渡される前にformatされる do_something() def sub2(global_opts, args): """Usage: {app_name} cmd1 sub2 [<args>...] """ do_something()
こんな感じの実装にしてみた。
今のところ自分のユースケースはカバーできているが、もう少しコマンドが増えてくると考慮しないといけないことが出てくるかもしれない。