pythonでargparseを使ったコマンドライン解析

状況説明

python で コマンドラインのオプションや引数を解析するのに、今時は argparse を使うようなのだが、よくある優先順位で

コマンドラインオプション >> 設定ファイル >> プログラム内デフォルト

ってどうやればいいのかわからなかったのでやってみたメモ。

(すんません、まだ2.7系使ってる情弱なもんでリンクがそっち向いてます。 こと argparse に関しては大差ないんじゃないかと。)

ソース

まずは python コードで、一応 myparser.py という名前にしておく。

  • ArgumentParser() でパーザ p を作って、
  • p に対して –config とか –param1 とか –param2 とかいうオプションがあるよと教え、
  • parse_args() で解析して、
  • その結果から –config オプションに与えられた値を取り出して
  • そいつがファイルならyamlだと思って読みだし、
  • 内容を引数群 (args) に 反映する。

この時に、設定ファイルのファイル名をコマンドライン引数から受け取るので 当然引数解析は先にやっておかねばならず、設定ファイルの内容をそのまま args に上書きすると、コマンドラインでの指定より設定ファイルでの指定の ほうが強くなってしまう、というのが問題。

# myparser.py
import argparse
import os
import yaml

p = argparse.ArgumentParser(description='sample parser')

p.add_argument('--config', help="configuration file [default: %(default)s]", default=os.path.expanduser("~/myconfig1.yml"))
p.add_argument('--param1', help="parameter1", default='param1')
p.add_argument('--param2', help="parameter2", default='param2')

args = p.parse_args()

if os.path.isfile(args.config):
    f = file(args.config)
    config = yaml.load(f)
    f.close()

    for i in config.keys():
        if getattr(args, i) == p.get_default(i):
            setattr(args, i, config[i])

print args

最後の方で getattr(args, i) と p.get_default(i) を比較しているところが あるがこれがミソで、getattr()はコマンドラインを解析した結果、p.get_default() は add_argument() で与えられた default 値だから、この比較の結果が「同じ」 ってことは「コマンドライン引数としては与えられていない」ってことになる。

で、その時だけは設定ファイルから持ってきた設定を args に書き込めば、 冒頭の優先順位ができるわけだ。

ただし、うまくいかない例外はあって、「設定ファイルには default 値と 異なる設定が書いてある」けれど「コマンドラインから default 値と同じ 設定を与えて上書きしたい」時、これだとうまくいかない。

実行例

$ python --version
Python 2.7.6

$ ls
myconfig1.yml  myconfig2.yml  myparser.py

$ cat myconfig1.yml
param1: foobar
param2: myconfig1.yml

$ cat myconfig2.yml
param1: hogehoge
param2: myconfig2.yml

$ python myparser.py
Namespace(config='/home/app/myconfig1.yml', param1='foobar', param2='myconfig1.yml')

$ python myparser.py --config=myconfig2.yml
Namespace(config='myconfig2.yml', param1='hogehoge', param2='myconfig2.yml')

$ python myparser.py --param1=GUGANGUGAN
Namespace(config='/home/app/myconfig1.yml', param1='GUGANGUGAN', param2='myconfig1.yml')

$ python myparser.py --config=myconfig2.yml --param1=GUGANGUGAN
Namespace(config='myconfig2.yml', param1='GUGANGUGAN', param2='myconfig2.yml')

$ python myparser.py --config=myconfig1.yml --param1=param1
Namespace(config='myconfig1.yml', param1='foobar', param2='myconfig1.yml')

コマンドラインオプション無しだと、default の設定ファイルの myconfig1.yml を 読むので、add_argument() で与えた default 値とはちょっと違う args になっている。

別の設定ファイル myconfig2.yml を与えればそっちの内容に従う。

(設定ファイルの指定なし、ということは default の myconfig1.yml を見ている 状態で) –param1=GUGANGUGAN を与えると、param1 だけ上書きされている。

設定ファイルに myconfig2.yml を指定して –param1=GUGANGUGAN を与えると param1 だけ上書きされる。まあ当然。

設定ファイルに myconfig1.yml を指定すると、この設定ファイル中で param1 は foobar だと書いてある。他方で myparser.py の add_argument() で与えた default は param1。 この時、設定ファイルでの指定をコマンドラインオプションで上書きして param1 に 戻したいとき、–param1=param1 を書きたくなるわけだが、これはうまくいかない。

よしなしごと

–config オプションを使うなら、–param1 のようなオプションを使わずに 全部設定ファイル内に書くようにすれば良い話だが、ちょっと納得が行かない。

argparse に特別のオプション(要するに –config をハードコードして特別扱い するようなコード) を追加しても良いのではないかと思うが、まあ、確かに美しくは ないのよね。

もっと良い書き方があったらどうぞ教えてやってください。m(_._)k

備考

  • 2015/Oct/20 ごろ書いた。