スクリプトの–versionオプションについて

※ 追記あり

argparse モジュールを使っているうち、気になった、というか気に入らないことがあったのでメモしておく。
とりあえず、こんなスクリプトがあったとする。

import argparse

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', action='store',
                    help='specify name instead "world".')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

これは以前 argparse を試してみたときのスクリプトにちょっと変更を加えたもので、どこを変えたかというと、位置引数 NAME のデフォルト値をなくして必須の引数にした点だ。

^o^ > python hello2.py -h
usage: hello2.py [-h] [-m] [-t N] NAME

Hello world program.

positional arguments:
  NAME             specify name instead "world".

optional arguments:
  -h, --help       show this help message and exit
  -m, --morning    good morning
  -t N, --times N  repeat N times

^o^ > python hello2.py Andy
Hello, Andy.

^o^ > python hello2.py -m Andy
Good morning, Andy.

^o^ > python hello2.py -m -t 3 Andy
Good morning, Andy.
Good morning, Andy.
Good morning, Andy.

^o^ > python hello2.py
usage: hello2.py [-h] [-m] [-t N] NAME
hello2.py: error: too few arguments

最後の実行例のように、NAME 引数を省略するとエラーになる。これは期待通りの動作。
で、もうひとつ覚えておいてほしいのは一番最初の -h オプションを指定した実行例。NAME 引数が無いにもかかわらずエラーにならずにヘルプが表示されている。これも期待通りの動作だ。

さて、本題はこれから。一通りの機能を実装し終わったら(そしてこれからも機能を拡張するつもりがあるなら)、スクリプトのバージョンを表示するオプションを追加したくなる。
というわけで、バージョンを表示する -v (–version) オプションを追加したのがこれだ。

import argparse

script_version = "v0.1.0"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

ところが、これは期待通りには動作しない。

^o^ > python hello2a.py -v
usage: hello2a.py [-h] [-v] [-m] [-t N] NAME
hello2a.py: error: too few arguments

NAME 引数が無いために、引数が足りないと怒られてしまう。
この問題は、NAME 引数を定義するところで、nargs=’?’ とすることでとりあえず回避できる。

import argparse

script_version = "v0.1.0"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', nargs='?', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)
^o^ > python hello2b.py -v
v0.1.0

が、一方でこれは別の問題を引き起こす。NAME 引数が無くてもスクリプトが動いてしまうのだ。これは期待している動作とは違う。

^o^ > python hello2b.py
Hello, None.

NAME 引数がない場合はちゃんとエラーになってほしい。でもって -v オプションを指定したときには NAME 引数が無くてもエラーになってほしくない。思い出してほしいが -h オプションはまさにそのように動作している。どうにかして期待通りに動作するようにはできないものだろうか。argparse のドキュメントを見たもののどうも解決策はなさそうに見える。

 cf. http://docs.python.jp/2/library/argparse.html

で、結局、NAME 引数が指定されているかどうかを自前でチェックするようにした。

import argparse

script_version = "v0.1.0"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', nargs='?', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', dest='version', action='store_true',
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

if args.version:
    print script_version
    exit()

if args.name is None:
    print parser.prog + ": error: too few arguments. type " + parser.prog + " -h for help"
    exit()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

これで期待通り。

^o^ > python hello2c.py -v
v0.1.0

^o^ > python hello2c.py
hello2c.py: error: too few arguments. type hello2c.py -h for help

でもやっぱり、argparse で何とかできてほしいなあ。

[追記]
argparse のドキュメントをよくよく読んでみたら、やっぱり解決策があった。
add_argument() の引数に、action=”version” と version=バージョンナンバー を指定すればいい。直したのがこれ:

import argparse

script_version = "v0.1.1"

parser = argparse.ArgumentParser(description='Hello world program.')
parser.add_argument('name', metavar='NAME', action='store',
                    help='specify name instead "world".')
parser.add_argument('-v', '--version', action='version', version=script_version,
                    help='show version and exit')
parser.add_argument('-m', '--morning', dest='greeting', action='store_const',
                    const='Good morning', default='Hello', help='good morning')
parser.add_argument('-t', '--times', dest='times', type=int, action='store', default=1,
                    metavar='N', help='repeat N times')
args = parser.parse_args()

for n in range(args.times):
    print '%s, %s.' % (args.greeting, args.name)

実行結果:

^o^ > python hello2d.py -h
usage: hello2d.py [-h] [-v] [-m] [-t N] NAME

Hello world program.

positional arguments:
  NAME             specify name instead "world".

optional arguments:
  -h, --help       show this help message and exit
  -v, --version    show version and exit
  -m, --morning    good morning
  -t N, --times N  repeat N times

^o^ > python hello2d.py -v
v0.1.1

^o^ > python hello2d.py Andy
Hello, Andy.

^o^ > python hello2d.py
usage: hello2d.py [-h] [-v] [-m] [-t N] NAME
hello2d.py: error: too few arguments