Pythonのコマンドラインオプションパーサーライブラリdocopt, schemaを使う

2013/11/26 | 所要時間 約4分

Pythonには標準でコマンドラインオプションを解析するモジュールがついてるんですが、 愚直に書いてるとすごくだらだらと長くなってしまうし、ぱっと見わかりづらいですよね。
それにPython2.7以上とかだったりで、結局古い方のモジュール使わないといけないし・・・とか。

そこで簡単にオプションをパースできるdocoptとschemaについて紹介します。

docopt

docopt - Pythonic argument parser, that will make you smile

docoptはPython2.5から3.3まで対応してます。

普通のパーサーはオプションのプログラムを書いて、そこからヘルプの文字列を生成したりするのですが、 docoptの場合はdocstringからパーサーを生成するという実にpythonicなライブラリです。

使い方は説明するより実際に試してみたほうが早いと思います。 以下のサイトで実際にdocstringを書いて実行することができます。

try.docopt.org

パースされるとDictionaryとして返ってくるのであとはそれを参照するだけです。

example

実際にdocoptを使用したツールを作ってみました。

ton1517/randfilter

ファイル(複数可)または標準入力から文字列を受け取り、ランダムに行を選択し出力するだけのツールです。

コマンドラインオプションのdocstringはrandfilter.pyの24行目からです。

"""
randfilter

Usage:
randfilter (-n <num> | -p <probability>) [-u | --unorder] [-i | --ignore-empty] [<files>...]
randfilter -h | --help
randfilter -v | --version

Options:
<files>...          Choose and output lines at random in files. If omitted, use stdin.
-n <num>            The choise number of lines.
-p <probability>    The choise probability of lines. The value is 0.0 to 1.0.
-u --unorder        Output lines are unordered.
-i --ignore-empty   Ignore empty line.
-h --help           Show this screen.
-v --version        Show version.
"""

(これをtry.docopt.orgにコピペして試すこともできます)

あとはmain関数で

from docopt import docopt
args = docopt(__doc__, version="{0} {1}".format(NAME, VERSION))

としているだけです。これだけでパースできます。

version引数はrandfilter --versionと打った時に表示する文字列を指定しているだけなので省略可です。

python randfilter.py -n 5 --unorder --ignore-empty README.rst setup.py

というオプションで実行すると以下の様なDictionaryになって返ってきます。

{'--help': False,
'--ignore-empty': True,
'--unorder': True,
'--version': False,
'-n': '5',
'-p': None,
'<files>': ['README.rst', 'setup.py']}

すごく簡単ですね。

しかし、docoptはオプションに与えられた値の検証までは行いません。そこでschemaです。

schema

schema - Simple data validation library

schemaは名前の通り、Pythonのデータ構造のスキーマを検証するライブラリです。 Python2.6から3.3まで対応しています。

詳しい説明は上記のページに載っているので、実際に使用した例を載せます。

example

これも先ほどのrandfilterで実際に使用した例です。

randfilter.pyのvalidate_args関数内のコードです。

schema = Schema({
    '-n': Or(None, And(Use(int), lambda n: 0 <= n), error="-n should be positive integer"),
    '-p': Or(None, And(Use(float), lambda n: 0.0 <= n <= 1.0), error="-p should be float 0 <= N <= 1.0"),
    '--unorder': bool,
    '--ignore-empty': bool,
    '--help': bool,
    '--version': bool,
    '<files>': [Use(open, error="Files should be readable")]
})

args = schema.validate(args)

最初にSchemaクラスのコンストラクタにスキーマ定義Dictionaryを渡して、 validate関数でdocoptのDictionaryを渡してやるだけです。

簡単に説明すると

  • -n : Noneまたは、intかつ0以上の値
  • -p : Noneまたは、floatかつ0.0以上1.0以下の値
  • <files> : リスト内の全てのファイル名がopen関数でファイルオープンできること
  • その他 : bool値であること

という定義になっています。Use()関数を使うと引数に指定した関数を実際に値に適用します。 ここでは<files>オプションで指定された文字列に対しopen()関数を実行するので、文字列の代わりにファイルオブジェクトが入ることに注意してください。(ファイルを開きたくない場合は[os.path.exists]としてファイルの存在を確認するだけで良いと思います。)

スキーマ定義に沿っていなかった場合はSchemaError例外を吐くので捕まえて終了させます。 スキーマ定義のerror引数に文字列を与えるとexecptでその文字列が渡されます。

try:
    args = validate_args(args)
except SchemaError as error:
    print(error)
    sys.exit(1)

docopt & schema のメリット

標準のモジュールを使わずにdocoptとschemaを使うことのメリットは表記が簡潔になるというのはもちろんですが、 一番のメリットはdocoptによってドキュメントとプログラムが同期している事が保証されるということだと思います。 docoptによって、プログラムを変更してもドキュメントが古いまま・・・というありがちなミスをなくすことができます。 (細かい説明文などは確認しないと古くなってしまう可能性があるので注意しないといけませんが。)

ドキュメントがプログラムによって取得できるというPythonの仕様は自分がPythonすげーと思った最初の仕様ですw

2013/11/26

プロフィールアイコン

ton

何でもやりたいエンジニア

趣味でFlash作って遊んでいたらプログラマーになってしまいました。

仕事ではSNSの運用したり、ゲーム作ったり、webサービス作ったり、アプリ作ったり、色々してます。