りんごがでている

何か役に立つことを書きます

Pythonのコマンドライン引数処理の決定版 docopt (あとJuliaに移植したよ)

Pythonをよく使う人にはよく知ってる人も多いのですが、docoptという便利ライブラリがあります。 docoptはargparseやoptparseのようなコマンドライン引数をパースするライブラリなのですが、その発想がコロンブスの卵なのです。

例えばPython標準のargparseだと、argparseのAPIを組み合わせてパーサを組み立てるわけです。するとパーサと一緒にヘルプも作ってくれて、"program --help"などとすると自動生成されたヘルプを表示してくれるようになります。 しかし、そのAPIを覚えるのが大変で、毎回ドキュメントを読まないと忘れちゃうわけです。

import argparse

parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
                   help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
                   const=sum, default=max,
                   help='sum the integers (default: find the max)')

args = parser.parse_args()
print(args.accumulate(args.integers))

https://docs.python.org/dev/library/argparse.html

その問題をdocoptではスマートに解決してくれます。docoptでは、パーサからヘルプを生成するのではなくて、ヘルプからパーサを生成します。そしてコマンドライン引数は辞書(dict)を使って取得できるようになります。 docoptモジュールがエクスポートしているのはdocopt関数のみで、複雑なAPIを覚える必要もなく、馴染みのあるヘルプを文法に従って書けばパーサを生成してくれます。

先の例をdocoptで書き換えれば、以下のようになります。

"""Process some integers.

usage: prog.py [-h] [--sum] N [N...]

options:
    -h, --help  show this help message and exit
    --sum        sum the integers (default: find the max)
"""

from docopt import docopt

if __name__ == '__main__':
    args = docopt(__doc__)
    Ns = map(int, args["N"])
    if args["--sum"]:
        print(sum(Ns))
    else:
        print(max(Ns))

とても直感的ではありませんか! 使用すると、以下のようになります。 引数が文法に合わなければ usage が表示されます。

/Users/kenta/tmp% python3 prog.py 4
4
/Users/kenta/tmp% python3 prog.py 4 3 5 2
5
/Users/kenta/tmp% python3 prog.py --sum 4 3 5 2
14
/Users/kenta/tmp% python3 prog.py
Usage: prog.py [-h] [--sum] N [N...]
/Users/kenta/tmp% python3 prog.py -h
Process some integers.

usage: prog.py [-h] [--sum] N [N...]

options:
    -h, --help  show this help message and exit
    --sum        sum the integers (default: find the max)

ヘルプの文法は標準的なもので、以下の様なものがあります。

要素 説明 補足
<argument> 位置引数 <x> <y>に引数1 2を与えれば、{"<x>": 1, "<y>": 2}と結び付けられる
--option, -o オプション ハイフンで始まる引数
(arguments) 必須の引数 引数はデフォルトで必須だが、グループ化するときに使える
[arguments] 必須でない引数 (...)と違って、なくてもエラーにはならない
arg1 | arg2 arg1もしくはarg2 3つ以上連ねることもできる (例. --up | --down | --left | --right)
args... 可変数引数 返り値はリストになる

これらを組み合わせて、以下のように複雑なものも書けます。これをargparseで書くのは至難の業でしょう。

Naval Fate.

Usage:
  naval_fate ship new <name>...
  naval_fate ship <name> move <x> <y> [--speed=<kn>]
  naval_fate ship shoot <x> <y>
  naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
  naval_fate -h | --help
  naval_fate --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.org)

docoptを使うモチベーションやより詳しい文法については公式サイト(http://docopt.org/)や作者によるPyCon UK 2012の動画を御覧ください。


PyCon UK 2012: Create beautiful command-line ...

言語非依存なのも利点の1つです。一度使い方を覚えれば、他言語でもAPIを特別覚える必要がなく使えます。 オリジナルのPythonの他にも、Haskell, Go, C, Ruby, CoffeeScriptなどにも移植されています。

Julia版のdocopt - DocOpt.jl

個人的に最近Juliaをよく使っているのですが、docoptが無かったので移植したしました。 GitHubに上げたのは何ヶ月か前なのですが、最近やっと正式なdocoptファミリーの一員になり、Juliaの正式なパッケージにしたので改めて紹介します。

ソースコードはこちらです。https://github.com/docopt/DocOpt.jl

文法や使い方はPython版と同じですので、既にPython版を使ったことのある方は何も覚えることはありません。 using DocOptしてdocopt関数をインポートし、ヘルプを渡すだけです。

doc = """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.

"""

using DocOpt  # import docopt function

arguments = docopt(doc, version=v"2.0.0")
dump(arguments)

パース結果はPython同様、辞書になります。

インストールはJuliaの対話環境から、Pkg.add("DocOpt")とすればインストールできます。 残念ながら今のところJuliaのv0.2系をサポートしていないので、v0.3のコンパイル済みprerelease版をインストールするか、ソースからビルドすることになります。 v0.2で使いたいという要望があれば、v0.2もサポートしようと思います。