見出し画像

Pythonでsubparserを使ってCLIアプリを洗練させる

何らかプログラムを書くとき、自分の場合は最近はWeb APIかあるいはバッチ処理のものが多い。
バッチ処理について言うと、メンテナンスできる可能性のある人が多いということやライブラリの充実度合いからPythonが選ばれる。
PythonでCLIを書くときに近頃調べて知ったサブコマンドというものがあり、それを使うと、多少なりともGitのCLIのような、このコマンドでは何ができるのか、が明示しやすくなるのでメモを残す。

まず、自分が普段一緒に仕事をしているチームでは、tqdmとargparseは定番だった。

Argparseは、CLIをCLIたらしめる、引数の受付けやRequired/Optionalの指定、各引数の説明を実現してくれるライブラリ。(しかも標準提供......!)
tqdmは、長く時間がかかる処理などを前提に、進み具合をプログレスバーのようなかたちで出してくれるので、処理中の時などにユーザーに情報提供するためには入れておきたいライブラリ。

で、しばらくそれらを使いながら過ごしていた結果感じた課題として、コマンドが複数に成長を始め、コマンドによって必要なオプションの種類が大きく分岐してきていたということ。
唐突だけど例えば、自販機の在庫状況を調べられるCLIを作っているとして、次のような3つのコマンドを用意しているとする。

1. 自販機全体の在庫を知る -c コマンド名 -i 自販機のID
2. 自販機のある商品の在庫を知る -c コマンド名 -i 自販機のID -p 商品のID
3. 全体として取り扱っているある商品の詳細を知る -c コマンド名 -p 商品のID

これを何の工夫もなく実現すると、1と2では必須の自販機のIDは3では不要で、必須と任意や不要なものが混ざる。
そうすると、すべての引数を任意で受け付けた上で、個別のコマンドの分岐内でオプションについて処理する必要が出てくる。
Less Elegantな作りになってしまうのだけれど、少しの間、自分たちのチームは少なくともそうだった。

そろそろOptimizationのタイミングだろう、ということで、次のような記事を確認した。

結果として、自分たちに不足していたのは、既に使用していたargparseに含まれるsubparserを使ったサブコマンドによる構成だったことがわかった。

https://docs.python.org/3.7/library/argparse.html?highlight=argparse#sub-commands

サンプルとして書き出してみるとこんな感じ。

import argparse
def main():
   parser = argparse.ArgumentParser(description="my awesome CLI client")
   subparsers = parser.add_subparsers(dest="command", help="Execution Command")
   
   subcommand1_parser = subparsers.add_parser("list", help="List all stocks")
   
   subcommand1_parser.add_argument("-v", "--verbose", required=False, action="store_true", help="enable verbose log output")
   subcommand1_req_args = subcommand1_parser.add_argument_group("required arguments")
   subcommand1_req_args.add_argument("-i", "--id", required=True, help="target id")
   
   args = parser.parse_args()
   command = args.command

if __name__ == "__main__":
   main()

例えば、src/main.pyにこの処理を書いていた場合、python src/main.py list -i id001とか呼び出せる。
もし python src/main.py listと打つと、必要な引数が足りないと言われるし、python src/main.py -hとかやると、追加したサブコマンド(今回だとlistがそれに相当)の候補一覧が見れる。

python src/main.py list
usage: main.py list [-h] [-v] -i ID
main.py list: error: argument -i/--id is required

python src/main.py -h
usage: main.py [-h] {list} ...
my awesome CLI client
positional arguments:
 {list}      Execution Command
   list      List all stocks
optional arguments:
 -h, --help  show this help message and exit

CLIとしての使いやすさに影響する要因は他にもあるかもしれないけれど、少なくともPythonではある程度までの使い勝手の良さを標準ライブラリの範疇で解決できる。
動的型付けのしんどさはmypyで解決するとして、あとは作ったアプリの配布やデプロイのしやすさが改善されると良いのだけど......それはまた別の話ということで。

posted on here: https://blog.tkhm.dev/2020/09/pythonsubparsercli.html