nikkie-ftnextの日記

イベントレポートや読書メモを発信

argparseでバージョン確認もサポートしたコマンドを作る(python script.py --version)

はじめに

かがみの孤城Blu-ray&DVD発売中🎉
全世代にお勧めしたい作品! nikkieです。

Pythonコマンドラインツールが作れる標準ライブラリのargparseについて、最近知った小ネタをアウトプットします。

目次

argparseでコマンドラインツールが作れる!

過去のエントリでも取り上げました。

指定した単語を指定した回数繰り返すコマンドラインツールです1

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("string")
parser.add_argument("num", type=int, nargs="?", default=2)
args = parser.parse_args()

print(args.string * args.num)

Python 3.11.1で動作確認しています。

% # 繰り返し回数を指定しないと2回繰り返す(デフォルト値)
% python repeat.py こころ
こころこころ
% # 繰り返し回数を指定する例
% python repeat.py こころ 5
こころこころこころこころこころ

コマンドのバージョンを確認したい

身の回りのコマンドを思い出すと、pythongitなど--versionでバージョンが確認できますよね。
バージョンを確認した時はコマンドのメイン処理は実行されません。
これを上記のrepeat.pyでも実現したいと思いました。

引数を追加してみると

+import sys
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("string")
parser.add_argument("num", type=int, nargs="?", default=2)
+parser.add_argument("--version", action="store_true")
args = parser.parse_args()

+if args.version:
+    print("version 0.1.0")
+    sys.exit(0)

print(args.string * args.num)
% python repeat.py --version
usage: repeat.py [-h] [--version] string [num]
repeat.py: error: the following arguments are required: string

あれ、バージョンが表示されない...
引数が足りないというエラーです。
なぜなら、string引数(位置引数。必須)を指定していないからです。

バージョンを表示したいだけなので、string引数の指定は不要にしたいです。

バージョンを確認できるようにしたスクリプト

以下のスクリプトで実現できます。

% python repeat.py --version
version 0.1.0
% python repeat.py こころ   
こころこころ
% python repeat.py こころ 5
こころこころこころこころこころ

ポイントは、部分解析!

バージョンの引数のために別のArgumentParserを追加し、それで部分解析をします。

version_parser = argparse.ArgumentParser(add_help=False)
version_parser.add_argument("--version", action="store_true")
args, unknown = version_parser.parse_known_args()

https://docs.python.org/ja/3/library/argparse.html#partial-parsing

ときどき、スクリプトコマンドライン引数のいくつかだけを解析し、残りの引数は別のスクリプトやプログラムに渡すことがあります。こういった場合、 parse_known_args() メソッドが便利です。

これは parse_args() と同じように動作しますが、余分な引数が存在してもエラーを生成しません。

実装としては

  • parse_known_args()が返したargsから--versionが指定されていたらバージョンを出力してコマンドを終了
  • --versionが指定されていなければ、unknown(引数からなるリスト)をコマンド本体のパーサでパース

としています。

ヘルプメッセージの調整

細かな点ですが、コマンド本体のパーサのparents引数にバージョンの引数のためのパーサを指定しているのもポイント!

parser = argparse.ArgumentParser(parents=[version_parser])

これにより、バージョン確認のオプションもヘルプメッセージに含まれます

% python repeat.py -h
usage: repeat.py [-h] [--version] string [num]

positional arguments:
  string
  num

options:
  -h, --help  show this help message and exit
  --version

https://docs.python.org/ja/3/library/argparse.html#parents

ときどき、いくつかのパーサーが共通の引数セットを共有することがあります。それらの引数を繰り返し定義する代わりに、すべての共通引数を持ったパーサーを ArgumentParser の parents= 引数に渡すことができます。

一番親になるパーサーに add_help=False を指定していることに注目してください。

比較のため、指定しない場合のヘルプメッセージです(parser = argparse.ArgumentParser())。
--versionの指定については記載がありませんね。

% python repeat.py -h      
usage: repeat.py [-h] string [num]

positional arguments:
  string
  num

options:
  -h, --help  show this help message and exit

部分解析を知ったのはpysenのソースコードリーディング

https://github.com/pfnet/pysen/blob/0.10.4/pysen/cli.py#L265-L270

    parser = _setup_manifest_parser()
    args, unknown = parser.parse_known_args()

    if args.version:
        _show_version()
        sys.exit(0)

終わりに

バージョンを確認するオプションを持ったコマンドラインツールをargparseで作る方法をアウトプットしました。

  • 単一のArgumentParserで実現しようとすると、--versionと合わせて必須引数の指定も必要になってしまう😢
  • そこで別のArgumentParser + 部分解析parse_known_args
    • ArgumentParser初期化時のparents引数を使ってヘルプメッセージにオプションも追加できる!

作り方がわかったことで、python --versiongit --versionと指定したときにバージョンだけが表示される(コマンド本体は実行されない)という作りって実は工夫がいるんだなと見逃していた点に気付きました。

こころちゃんこころちゃんこころちゃんこころちゃんこころちゃんこころちゃんこころちゃんこころちゃんこころちゃんこころちゃん


  1. Pythonプロジェクトとしてentry point相当の指定をすることで、python script.pyから卒業して、コマンドで実行できるようになります