はじめに
最近、UDS診断ツール(uds-client)をClaude.aiと一緒に開発したところ、click というライブラリが使われていました。
コードを見ると @ 記号が至るところに出てきます。
@click.group()
@click.option("--ip", required=True, help="ECU IP address")
@click.pass_context
def cli(ctx, ip):
...
「この @ は何?」「clickって何をするライブラリ?」というところから整理します。
@ はデコレータ
Pythonの @ 記号はデコレータと呼ばれる構文です。「関数に機能を追加する」ための仕組みで、@xxx を関数の直前に書くと、その関数の振る舞いを変えたり拡張したりできます。
clickはこのデコレータをうまく活用して、CLIツールを宣言的に書けるように設計されています。
clickとは
click はPythonでCLIツール(コマンドラインツール)を作るためのライブラリです。
CLIツールとは、こういうコマンドで動くプログラムのことです。
$ python main.py --ip 192.168.1.1 rdbi F190
clickを使うと、オプションの定義・型チェック・ヘルプ生成・サブコマンドの管理などを、シンプルなコードで実現できます。
インストールは以下のコマンドで行います。
pip install click
clickで何ができるか
uds-clientの実際のコードをもとに説明します。
コマンドグループの定義
import click
DOIP_PORT = 13400
@click.group()
@click.option("--ip", required=True, help="ECU IP address")
@click.option("--port", default=DOIP_PORT, show_default=True, help="DoIP port")
@click.option("--log", default="INFO", show_default=True,
type=click.Choice(["DEBUG", "INFO", "WARNING"], case_sensitive=False),
help="Log level")
@click.pass_context
def cli(ctx, ip, port, log):
"""UDS診断ツール (DoIP)"""
ctx.ensure_object(dict)
ctx.obj["ip"] = ip
ctx.obj["port"] = port
ctx.obj["log"] = log
@click.group() を付けると、この関数が「複数のサブコマンドを束ねる親」になります。
@click.option() でオプションを定義します。required=True で必須チェック、default= でデフォルト値、type=click.Choice(...) で選択肢の制限が自動でかかります。
@click.pass_context を付けると、ctx(コンテキスト)オブジェクトが関数に渡されます。ctx.obj に値を入れておくことで、後述するサブコマンドからその値を参照できます。
サブコマンドの定義
@cli.command()
@click.argument("did")
@click.pass_context
def rdbi(ctx, did):
"""0x22 ReadDataByIdentifier 例: rdbi F190"""
ip = ctx.obj["ip"] # 親で受け取った --ip の値が使える
...
click.echo(f"[OK] DID={did} IP={ip}")
@cli.command() を付けると、この関数がサブコマンドになります。関数1つ=サブコマンド1つという対応です。
@click.argument() は位置引数(rdbi F190 の F190 の部分)を受け取ります。オプション(--ip)と違い、-- を付けずに渡す引数です。
ctx.obj["ip"] で親コマンドで受け取った --ip の値を参照できます。
フラグオプション
@cli.command("tester-present") # Pythonの関数名はアンダースコア、CLIコマンド名はハイフンが慣習
@click.option("--suppress", is_flag=True, default=False, help="suppress response")
@click.pass_context
def tester_present(ctx, suppress):
"""0x3E TesterPresent"""
...
is_flag=True にすると、オプションが「付けたか・付けなかったか」の True / False で受け取れます。--suppress を付ければ True、付けなければ False です。
エントリーポイント
if __name__ == "__main__":
cli()
最後にこの1行を書くだけで、python main.py で動くCLIツールになります。
ヘルプが自動生成される
clickのもう一つの便利な点は、--help が自動で生成されることです。関数のdocstringがそのまま説明文として使われます。
$ python main.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...
UDS診断ツール (DoIP)
Options:
--ip TEXT ECU IP address [required]
--port INTEGER DoIP port [default: 13400]
--log [DEBUG|INFO|WARNING] Log level [default: INFO]
--help Show this message and exit.
Commands:
clear-dtc 0x14 ClearDiagnosticInformation
rdbi 0x22 ReadDataByIdentifier
read-dtc 0x19 ReadDTCInformation
reset 0x11 ECUReset
routine 0x31 RoutineControl
session 0x10 DiagnosticSessionControl
tester-present 0x3E TesterPresent
wdbi 0x2E WriteDataByIdentifier
同じことはargparseでもできる
実はここまで説明してきたことは、Pythonの標準ライブラリである argparse でも実現できます。
import argparse, sys
parser = argparse.ArgumentParser(description="UDS診断ツール")
parser.add_argument("--ip", required=True)
parser.add_argument("--port", type=int, default=13400)
subparsers = parser.add_subparsers(dest="command")
rdbi_parser = subparsers.add_parser("rdbi")
rdbi_parser.add_argument("did")
args = parser.parse_args()
if args.command == "rdbi":
print(f"[OK] DID={args.did} IP={args.ip}")
elif args.command is None:
parser.print_help()
sys.exit(1)
ただし、argparseには次の課題があります。
- サブコマンドの定義・処理・分岐が3箇所に分散する
- サブコマンドが増えるたびに
elifが増えていく -
--ipのような共通オプションをサブコマンドに渡す仕組みが自前になる
clickはこれらをデコレータで解決しています。uds-clientのようにサブコマンドが8個あるケースでは、その差がはっきり出ます。
まとめ
| 観点 | argparse | click |
|---|---|---|
| 依存 | 標準ライブラリ | pip install必要 |
| サブコマンド追加 | 定義・処理・分岐の3箇所 | 関数1つ追加するだけ |
| 共通オプションの伝播 | 手動 | ctx.objで自動 |
| ヘルプ生成 | 自動 | 自動(docstringがそのまま) |
| 向いている用途 | 小規模・単機能 | サブコマンドが複数あるツール |
小規模スクリプトならargparse、複数のサブコマンドを持つCLIツールを作るならclick、という使い分けが自分の中で整理できました。