実行環境
- Python 3.9.7
はじめに
argparseでCLIを作っています。
デフォルトの設定だと、CLIのヘルプメッセージが分かりづらいです。
例として、以下のCLIのヘルプメッセージを表示してみます。
from argparse import ArgumentParser
def parse_args():
parser = ArgumentParser(
description=textwrap.dedent(
"""
イメージを作成する。
``aws ec2 create-image`` コマンドを参考にした。
"""
).strip()
)
parser.add_argument(
"--name",
type=str,
required=True,
help=textwrap.dedent(
"""
イメージの名前。
制約:3-128文字の英数字。
"""
).strip(),
)
parser.add_argument(
"--type",
type=str,
choices=["foo", "bar"],
default="bar",
help=textwrap.dedent(
"""
イメージのタイプ。
* foo: FOO
* bar: BAR
"""
).strip(),
)
parser.add_argument(
"--dry-run", action="store_true", help="イメージを作らずに、イメージを作る権限があるかどうかをチェックする。"
)
return parser.parse_args()
if __name__ == "__main__":
parse_args()
※ textwrap.dedent("""...""").strip()
としているのは、ヒアドキュメントの前後の空白を取り除くためです。
$ python cli.py -h
usage: cli.py [-h] --name NAME [--type {foo,bar}] [--dry-run]
イメージを作成する。 ``aws ec2 create-image`` コマンドを参考にした。
optional arguments:
-h, --help show this help message and exit
--name NAME イメージの名前。 制約:3-128文字の英数字。
--type {foo,bar} イメージのタイプ。 * foo: FOO * bar: BAR
--dry-run イメージを作らずに、イメージを作る権限があるかどうかをチェックする。
以下の点で、ヘルプメッセージは分かりづらいです。
- コマンドの説明、引数の説明が改行されていない
-
--type
にデフォルト値が表示されていない
この2点を改善する方法を紹介します。
改善1:フォーマッタークラスを指定する
argparseには4つのフォーマットクラスが用意されています。これらのフォーマットクラスを複数継承したクラスを、ArgumentParser
コンストラクタのformatter_class
引数に指定します。
from argparse import ArgumentParser, RawTextHelpFormatter, RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter
class MyHelpFormatter(RawTextHelpFormatter, RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter):
pass
def parse_args():
parser = ArgumentParser(
description="""
イメージを作成する。
``aws ec2 create-image`` コマンドを参考にした。
""",
formatter_class=MyHelpFormatter
)
- RawTextHelpFormatter: 引数の説明で空白と改行がそのまま表示する。
- RawDescriptionHelpFormatter: descriptionとepilogで空白と改行がそのまま表示する。
- ArgumentDefaultsHelpFormatter: デフォルト値を表示する
$ python cli.py -h
usage: cli.py [-h] --name NAME [--type {foo,bar}] [--dry-run]
イメージを作成する。
``aws ec2 create-image`` コマンドを参考にした。
optional arguments:
-h, --help show this help message and exit
--name NAME イメージの名前。
制約:3-128文字の英数字。 (default: None)
--type {foo,bar} イメージのタイプ。
* foo: FOO
* bar: BAR (default: bar)
--dry-run イメージを作らずに、イメージを作る権限があるかどうかをチェックする。 (default: False)
コマンドの説明、引数の説明が改行されて、かつデフォルト値も表示されました。
設定2:引数と引数の説明の間に空行を入れる
引数の説明を改行して表示すると、引数と引数の区切りが分かりづらくなります。
なので、引数と引数の説明の間に空行を入れます。
先ほど作ったMyHelpFormatter
クラスに_format_action
メソッドを定義して、オーバライドします。
_format_action
メソッドは、argparse.Action
クラスを文字列に変換する関数です。
親クラスの_format_action
メソッドの結果に対して、改行\n
を付与することで、引数と引数の説明の間に空行ができます。
class MyHelpFormatter(
RawTextHelpFormatter, RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter
):
def _format_action(self, action: argparse.Action) -> str:
return super()._format_action(action) + "\n"
$ python cli.py -h
...
optional arguments:
-h, --help show this help message and exit
--name NAME イメージの名前。
制約:3-128文字の英数字。 (default: None)
--type {foo,bar} イメージのタイプ。
* foo: FOO
* bar: BAR (default: bar)
--dry-run イメージを作らずに、イメージを作る権限があるかどうかをチェックする。 (default: False)
設定3:不要なデフォルト値を表示しない
上記のヘルプメッセージでは、すべての引数に対してデフォルト値が表示されます。
しかし、本当に表示して欲しいデフォルト値は--type
だけです。
--name
のデフォルト値None
、--dry-run
のデフォルト値False
は自明なので、不要です。
MyHelpFormatter
クラスに_get_help_string
メソッドを定義して、オーバライドして対応します。
まずは、ArgumentDefaultsHelpFormatter
クラスの_get_help_string
メソッドの中身を、そのまま持ってきます。
ヘルプメッセージにデフォルト値を追加する条件に、action.default is not None and not action.const
を追加します。
action.const
は、以下の場合にTrueになります。
-
parser.add_argument
のaction引数にstore_true
やstore_false
を指定したとき -
parser.add_argument
のconst引数に値を指定したとき
class MyHelpFormatter(
RawTextHelpFormatter, RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter
):
...
def _get_help_string(self, action):
help = action.help
if "%(default)" not in action.help:
if action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
# カスタム設定
if action.default is not None and not action.const:
help += " (default: %(default)s)"
return help
$ python cli.py -h
usage: cli.py [-h] --name NAME [--type {foo,bar}] [--dry-run]
イメージを作成する。
``aws ec2 create-image`` コマンドを参考にした。
optional arguments:
-h, --help show this help message and exit
--name NAME イメージの名前。
制約:3-128文字の英数字。
--type {foo,bar} イメージのタイプ。
* foo: FOO
* bar: BAR (default: bar)
--dry-run イメージを作らずに、イメージを作る権限があるかどうかをチェックする。
--type
のみデフォルト値が表示されました。
設定4:必須かどうかを明記する
コマンドライン引数が必須かどうかは、ヘルプメッセージの"usage"を見れば分かります。
usage: cli.py [-h] --name NAME [--type {foo,bar}] [--dry-run]
しかし、コマンドライン引数が多いと、"usage"は少し分かりづらいです。
そこで、コマンドライン引数の説明に、必須ならrequired
と表示しましょう。
_get_help_string
メソッドに、以下のコードを追記します。
class MyHelpFormatter(
RawTextHelpFormatter, RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter
):
def _get_help_string(self, action):
help = action.help
if action.required:
help += " (required)"
if "%(default)" not in action.help:
...
return help
$ python cli.py -h
イメージを作成する。
``aws ec2 create-image`` コマンドを参考にした。
optional arguments:
-h, --help show this help message and exit
--name NAME イメージの名前。
制約:3-128文字の英数字。 (required)
--type {foo,bar} イメージのタイプ。
* foo: FOO
* bar: BAR (default: bar)
--name
にrequired
が表示されました。
まとめ
以下のヘルプフォーマットクラスを利用すれば、ヘルプメッセージが見やすくなります。
import argparse
from argparse import (OPTIONAL, SUPPRESS, ZERO_OR_MORE,
ArgumentDefaultsHelpFormatter, ArgumentParser,
RawDescriptionHelpFormatter, RawTextHelpFormatter)
class MyHelpFormatter(
RawTextHelpFormatter, RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter
):
def _format_action(self, action: argparse.Action) -> str:
return super()._format_action(action) + "\n"
def _get_help_string(self, action):
help = action.help
if action.required:
help += " (required)"
if "%(default)" not in action.help:
if action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
if action.default is not None and not action.const:
help += " (default: %(default)s)"
return help
改善できなかったこと
"usage"を適切に改行して見やすくする
コマンドライン引数が多いとき、ヘルプメッセージの"usage"は以下のように引数ごとに改行されれば、見やすくなります。
usage: cli.py [-h]
--name NAME
[--type {foo,bar}]
[--dry-run]
argparse.HelpFormatter
クラスの_format_actions_usage
メソッドの以下の部分を修正すれば、改行を入れることができそうです。
text = ' '.join([item for item in parts if item is not None])
# ↓
text = '\n'.join([item for item in parts if item is not None])
_format_actions_usage
をオーバライドするには、HelpFormatter._format_actions_usage
の中身をMyHelpFormatter
クラスに持ってくる必要があります。
しかし、HelpFormatter._format_actions_usage
の中身はステップ数が多いため、できればMyHelpFormatter
クラスに持ってきたくありません。バグの温床になりそうです。
したがって、この改善は諦めました。