Pythonistaなら知ってるオプションパーサ


この記事は[Python Advent Calendar 2014 - Qiita](http://qiita.com/advent-calendar/2014/python) 2日目の記事です
前日は @kureikei さんの[Blender関連](http://pronama.azurewebsites.net/2014/12/01/python-pronama-chan-blender-add-on/) でした

最近はgolangでツールを作るのが流行っていますが、負けじとpythonももっと盛り上がって欲しいですね
ということで、コマンドラインツールを作る時に必要な引数・オプションパーサを紹介していきます

# コンテンツ

Pythonからコマンドラインでの引数・オプションを処理します
使用するものは

1. `sys.argv`
1. `argparser.ArgumentParser`
1. `docopt.docopt`

の3種類
`getopt`しか使えないような古いPythonは切り捨てました
`optparse`はdeprecatedになっているので、ここでは紹介しません
[Deprecation of optparse](http://legacy.python.org/dev/peps/pep-0389/#deprecation-of-optparse)


# 対応バージョン

`argparse`がPython2.7、Python3.2で追加されていて、
`docopt`は[docopt/docopt](https://github.com/docopt/docopt#installation)によると
> docopt is tested with Python 2.5, 2.6, 2.7, 3.2, 3.3 and PyPy.

とあります。
手元では3.4でも動きました
コマンドラインツールとなるとどのバージョンを選ぶべきか難しいところですが、
2.7系がまだ有力かもしれません(*要出典)が、今後のことを見据えて3.4系使っていきたいですね

# 題材

必須の引数1つ取り、それをそのまま`print`する、というシンプルなコマンドラインツールを作ります
オプションとして

- `-h`: `--help`でヘルプ
- `-v`: `--verbose`でうるさい出力にする
- `-c <arg>`, `--cat <arg>`: 引数と`<arg>`を結合して出力する

という3つを実装しました

```zsh
python hoge.py -h
# => ヘルプの表示
python hoge.py foo
# => input is foo
python hoge.py foo -v
# => your input is foo!!!
python hoge.py foo -c bar
# => concatenated: foobar
```

# sys.argvを使う

一番基本的なやり方でしょう
引数やオプションはすべて`sys.argv`のindexで判断することとなります
今回は必須の引数があるため、`sys.argv[1]`を決め打ちでその引数として扱えます
オプションは`-`で始まるため、`startswith('-')`で判断できます

```py:sys_parser.py
import sys

def parser():
    usage = 'Usage: python {} FILE [--verbose] [--cat <file>] [--help]'\
            .format(__file__)
    arguments = sys.argv
    if len(arguments) == 1:
        return usage
    # ファイル自身を指す最初の引数を除去
    arguments.pop(0)
    # 引数として与えられたfile名
    fname = arguments[0]
    if fname.startswith('-'):
        return usage
    # - で始まるoption
    options = [option for option in arguments if option.startswith('-')]

    if '-h' in options or '--help' in options:
        return usage
    if '-v' in options or '--verbose' in options:
        return 'your input is {}!!!'.format(fname)
    if '-c' in options or '--cat' in options:
        cat_position = arguments.index('-c') \
                if '-c' in options else arguments.index('--cat')
        another_file = arguments[cat_position + 1]
        return 'concatnated: {}{}'.format(fname, another_file)
    return 'input is {}'.format(fname)

if __name__ == '__main__':
    result = parser()
    print(result)
```

引数の順番が変わったりすると全く対応できなくなってしまうが、おぼえることが最小限で済むため、簡単に使える
ほんのちょっとしたオプションを処理したい、とかの時はこれで十分だと思います

helpは`usage`で定義した文字列がそのまま表示されます

```sh
$ python sys_parser.py -h
# => Usage: python sys_parser.py FILE [--verbose] [--cat <file>] [--help]
```

# argparseを使う

具体的には`argparse.ArgumentParser`です
`add_argument`で色々と細かな設定が出来るようになっています
`required=True`で必須項目としたり、`dest`で変数の保存先を指定したり、真偽値を保存したり、変数の型を指定したり出来ます

```py:argparse_parser.py
from argparse import ArgumentParser

def parser():
    usage = 'Usage: python {} FILE [--verbose] [--cat <file>] [--help]'\
            .format(__file__)
    argparser = ArgumentParser(usage=usage)
    argparser.add_argument('fname', type=str,
                           help='echo fname')
    argparser.add_argument('-v', '--verbose',
                           action='store_true',
                           help='show verbose message')
    argparser.add_argument('-c', '--cat', type=str,
                           dest='another_file',
                           help='concatnate target file name')
    args = argparser.parse_args()
    if args.verbose:
        return 'your input is {}!!!'.format(args.fname)
    if args.another_file:
        return 'concatenated: {}{}'.format(args.fname, args.another_file)
    return 'input is {}'.format(args.fname)

if __name__ == '__main__':
    result = parser()
    print(result)
```

順番に関係なく引数を処理できるようになる点と、引数の型を指定できる点がメリットだと思います

helpは`help=...`で定義したものもいい感じに表示してくれます

```sh
$ python argument_parser.py -h
usage: Usage: python argument_parser.py FILE [--verbose] [--cat <file>] [--help]

positional arguments:
  fname                 echo fname

optional arguments:
  -h, --help            show this help message and exit
  -v, --verbose         show verbose message
  -c ANOTHER_FILE, --cat ANOTHER_FILE
                        concatnate target file name
```

サブコマンドを実装することも出来ます
[16.4. argparse — コマンドラインオプション、引数、サブコマンドのパーサー — Python 3.4.2 ドキュメント](http://docs.python.jp/3/library/argparse.html#sub-commands)

# docoptを使う

[docopt/docopt](https://github.com/docopt/docopt)
docstringに使い方を書けば、それをparseしてインターフェイスを作ってくれます
unixコマンドなどのドキュメントに慣れている人なら、抵抗なく受け入れられるはず

```py:docopt_parser.py
__doc__ = """{f}

Usage:
    {f} <fname> [-v | --verbose] [-c | --cat <another_file>]
    {f} -h | --help

Options:
    -c --cat <ANOTHER_FILE>  concatnate target file name
    -v --verbose             Show verbose message
    -h --help                Show this screen and exit.
""".format(f=__file__)

from docopt import docopt


def parse():
    args = docopt(__doc__)
    if args['--verbose']:
        return 'your input is {}!!!'.format(args['<fname>'])
    if args['--cat']:
        return 'concatenated: {}{}'.format(args['<fname>'],
                                          args['--cat'][0])
    return 'input is {}'.format(args['<fname>'])


if __name__ == '__main__':
    result = parse()
    print(result)
```

ドキュメントが実装となるので、コードとドキュメントが同居するPythonらしいといえます
慣れていないとdocstringの書き方がやや難しく感じますが、非常に柔軟に引数を処理出来て、
また、`git add`みたいなコマンドを作ることも簡単に出来る点もメリットで、
上の例だと`<fname>`から`<>`を除いて`fname`にすれば、`fname`というコマンドとなります

helpは`__doc__`に書いたものがそのまま表示されます

```sh
$ python docopt_parser.py -h
docopt_parser.py

Usage:
    docopt_parser.py <fname> [-v | --verbose] [-c | --cat <another_file>]
    docopt_parser.py -h | --help

Options:
    -c --cat <ANOTHER_FILE>  concatnate target file name
    -v --verbose             Show verbose message
    -h --help                Show this screen and exit.
```

# 所感

- 本当に単純なものなら`sys.argv`で十分っぽい
- 少し複雑なら`docopt`使うと良さそう
	- `args['<fname>']`とかするの気持ち悪ければ`argparse`もアリ
	- documentの書き方で詰まったりするの嫌なら`argparse`の方が簡単
- `Ansible`なんかでは`sys.argv`と`argparse`と`optparse`の3つを使ってるようです
	- https://github.com/ansible/ansible/search?utf8=✓&q=optparse&type=Code
	- https://github.com/ansible/ansible/search?utf8=✓&q=argparse&type=Code
	- https://github.com/ansible/ansible/search?utf8=✓&q=optparse&type=Code

[Python Advent Calendar 2014 - Qiita](http://qiita.com/advent-calendar/2014/python) 明日は @akiniwa さんです