petitviolet blog

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

    2014-12-02

    QiitaPythoncliCUI

    この記事はPython Advent Calendar 2014 - Qiita 2 日目の記事です 前日は @kureikei さんのBlender 関連 でした

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

    コンテンツ

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

    1. sys.argv
    2. argparser.ArgumentParser
    3. docopt.docopt

    の 3 種類 getoptしか使えないような古い Python は切り捨てました optparseは deprecated になっているので、ここでは紹介しません Deprecation of optparse

    対応バージョン

    argparseが Python2.7、Python3.2 で追加されていて、 docoptdocopt/docoptによると

    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 つを実装しました

    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('-')で判断できます

    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で定義した文字列がそのまま表示されます

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

    argparse を使う

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

    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=...で定義したものもいい感じに表示してくれます

    $ 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 ドキュメント

    docopt を使う

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

    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__に書いたものがそのまま表示されます

    $ 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.
    

    所感

    Python Advent Calendar 2014 - Qiita 明日は @akiniwa さんです

    from: https://qiita.com/petitviolet/items/aad73a24f41315f78ee4