Python の argparse でサブコマンドを定義する(git風のCLI)

git や docker のように、1 つのコマンドの下に複数のサブコマンドを持つ CLI ツールは多い。git commitgit push のように、操作ごとにコマンド体系を分けることで、複雑な処理を整理できる。Python の argparse には add_subparsers というメソッドがあり、こうしたサブコマンド構造を簡単に実現できる。

add_subparsers の基本

サブコマンドを定義するには、まずメインのパーサーに対して add_subparsers() を呼び出し、そこに add_parser() でサブコマンドを追加していく。

import argparse

parser = argparse.ArgumentParser(description="ファイル管理ツール")
subparsers = parser.add_subparsers(dest="command")

# サブコマンド: add
add_parser = subparsers.add_parser("add", help="ファイルを追加する")
add_parser.add_argument("filename", help="追加するファイル名")

# サブコマンド: remove
rm_parser = subparsers.add_parser("remove", help="ファイルを削除する")
rm_parser.add_argument("filename", help="削除するファイル名")

args = parser.parse_args()
print(args)

dest="command" を指定すると、args.command にどのサブコマンドが選ばれたかが格納される。これがないと、どのサブコマンドが実行されたか判別できなくなるため、原則として指定しておくべきだ。

実行すると、次のように動作する。

$ python tool.py add memo.txt
Namespace(command='add', filename='memo.txt')

$ python tool.py remove old.txt
Namespace(command='remove', filename='old.txt')

サブコマンドごとに異なる引数を定義できるのが大きな利点で、add には filename だけ、remove には --force フラグを追加する、といった柔軟な設計が可能になる。

サブコマンドに関数を紐づける

実用的な CLI ツールでは、サブコマンドごとに処理関数を紐づけるのが一般的だ。set_defaults(func=...) を使うと、パース結果から直接その関数を呼び出せる。

import argparse

def do_init(args):
    print(f"プロジェクトを初期化: {args.name}")

def do_build(args):
    target = "release" if args.release else "debug"
    print(f"ビルド実行: {target}")

parser = argparse.ArgumentParser(description="プロジェクト管理")
subparsers = parser.add_subparsers(dest="command")

# init サブコマンド
init_parser = subparsers.add_parser("init", help="プロジェクトの初期化")
init_parser.add_argument("name", help="プロジェクト名")
init_parser.set_defaults(func=do_init)

# build サブコマンド
build_parser = subparsers.add_parser("build", help="プロジェクトのビルド")
build_parser.add_argument("--release", action="store_true", help="リリースビルド")
build_parser.set_defaults(func=do_build)

args = parser.parse_args()

if hasattr(args, "func"):
    args.func(args)
else:
    parser.print_help()

hasattr(args, "func") でチェックしているのは、サブコマンドなしで実行された場合の対策だ。サブコマンドが指定されなければ func 属性が存在しないため、ヘルプを表示するようにしている。

set_defaults を使う方法

サブコマンドごとに関数を紐づけ、args.func(args) で呼び出す。コードが分散せず、サブコマンドの追加が容易

if-elif で分岐する方法

args.command の値を見て分岐する。小規模なら問題ないが、サブコマンドが増えると分岐が肥大化する

小規模な CLI なら if-elif でも十分だが、サブコマンドが 5 個、10 個と増えてくると set_defaults 方式のほうが圧倒的に管理しやすくなる。

ヘルプメッセージの確認

argparse のサブコマンドは、ヘルプも自動的に構造化される。メインコマンドのヘルプでサブコマンド一覧が表示され、各サブコマンドにも個別のヘルプが用意される。

$ python tool.py --help
usage: tool.py [-h] {init,build} ...

プロジェクト管理

positional arguments:
  {init,build}
    init        プロジェクトの初期化
    build       プロジェクトのビルド

$ python tool.py build --help
usage: tool.py build [-h] [--release]

options:
  -h, --help  show this help message and exit
  --release   リリースビルド

add_parser に渡した help 引数がサブコマンドの説明として表示される仕組みだ。利用者にとってわかりやすい CLI を作るには、この help 文字列を丁寧に書いておくことが重要になる。

required オプション

Python 3.7 以降では、add_subparsers(dest="command") だけではサブコマンドの指定が必須にならない。サブコマンドなしで実行してもエラーにならず、args.commandNone になるだけだ。サブコマンドの指定を強制したい場合は required=True を加える。

subparsers = parser.add_subparsers(dest="command", required=True)

これにより、サブコマンドを省略するとエラーメッセージが表示されるようになる。ただし Python 3.6 以前では required 引数がサポートされていない点に注意が必要だ。

なお、Python 3.3 から 3.6 にかけてはサブコマンドがデフォルトで任意になるという仕様変更があり、それ以前はサブコマンドの指定が必須だった。

Python 3.2 まではサブコマンド未指定でエラーになっていたが、3.3 で意図せず任意に変わった経緯がある。

サブコマンドを使うと、1 つのスクリプトで多機能な CLI ツールを構築できる。小さなユーティリティから本格的な開発ツールまで、用途に応じた柔軟な設計が可能になるだろう。