Clickを利用したPython CLIツール開発
はじめに
こんにちは、藤本です。
先日エントリしたCuratorによるElasticsearchのメンテナンスにてCuratorのソースコード読んでる時に知ったClickというCLIツール作成支援ライブラリをご紹介します。
Pythonのコマンドラインパーサー
PythonはCLIツールによく利用される言語の一つです。例えば、aws-cliやOpenStackClient、先日記事にしたCuratorといったCLIツールもPython実装です。私もCLIツールを作成する時はほぼPythonで書きます。
CLIツールを実装する時にサブコマンド、引数の実装、バリデーション処理、Usageの定義など全て自力で実装しようとするとかなり面倒です。そこでPythonでは標準ライブラリでコマンドラインパーサーを提供しています。2.3系から追加されたoptparseは2.7系から非推奨となり、現在は2.7系から追加されたargparseが広く利用されていると思っています。私もargparseを利用していました。今回ご紹介するClickはサードパーティー製ライブラリではありますが、argparseの機能はほぼ実装されていて、私は機能面よりも書きやすさに惹かれました。
Click
- 公式ドキュメント
- ソースコードリポジトリ
公式ドキュメントの冒頭に記載されているようにClickを利用することで必要最小限のソースコードで使い易いCLIを実装することができます。
Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the "Command Line Interface Creation Kit". It's highly configurable but comes with sensible defaults out of the box.
argparseとの比較
Python標準モジュールとの違いは何なのでしょうか?
公式ドキュメントに以下に記載がありました。
- argparse has built-in magic behavior to guess if something is an argument or an option. This becomes a problem when dealing with incomplete command lines as it’s not possible to know without having a full understanding of the command line how the parser is going to behave. This goes against Click’s ambitions of dispatching to subparsers.
- argparse currently does not support disabling of interspersed arguments. Without this feature it’s not possible to safely implement Click’s nested parsing nature.
うーん、わかりづらい。。
私が個人的に気に入っているところは、コマンドラインパーサーの定義をDecoratorにより設定するため、設定にプログラムな要素が強いargparseに比べて、プログラムの可読性の高さを保つことができるように感じられました。まぁ、この辺は好みの問題もあると思います。
やってみた
以下のブログがかなり網羅して検証しているので、下記を読んでいただければ、ここで書くことがない気もしますが。。
本エントリではargparseと比較しながら実装方法をご紹介します。
インストール
PyPIに公開されていますので、pipでインストールできます。
# pip install click Collecting click Installing collected packages: click Successfully installed click-6.6
2016/06/09現在公開されているClickのバージョンは6.6です。
オプションを受け取る実装
まずはCLIツールでは間違いなく利用するであろうオプションの実装です。argparse
、Click
といったコマンドラインパーサーを利用することで、定義するオプションの引数を与えることで、型指定、必須属性、利用可能文字などのバリデーションチェック設定、Helpの設定を用意に行うことできます。
argparseの場合
まずはargparseによる実装です。
#!/usr/bin/env python import argparse def main(): parser = argparse.ArgumentParser(description='Simple argparse CLI') # (1) parser.add_argument('-n', '--name', dest='name', type=str, help='your name', required=True) # (2) parser.add_argument('-l', '--last-name', dest='last_name', type=str, help='your last name', default='Fujimoto', required=False) args = parser.parse_args() # (3) print("Your name is %s %s" % (args.name, args.last_name)) if __name__ == '__main__': main()
ArgumentParser
クラスでコマンドラインパーサーを定義します。argparseを利用してコマンドラインパーサーを実装する場合、ここから始まります。-
add_argument
メソッドでオプションを追加します。オプションの数だけこのメソッドにより追加します。最初の引数でオプションの指定文字列を指定します。,
区切りでAliasで複数指定可能です。dest
は引数を設定する変数名、type
は受け取る値の型、help
はこのあとご紹介するusageに出力される説明、required
はオプションが必須かどうか、default
はオプションを省略した時に設定される値、となります。他の設定項目は公式ドキュメントをご参照ください。直感的に実装を理解できるのは嬉しいですね。 -
作成した
AugumentParser
インスタンスからparse_args
メソッドで引数を取り出すことができます。
usageの表示
コマンドラインパーサーは自動で--help
オプションが実装されます。add_argument
メソッドに指定した引数に応じた表示がされます。実装せずとも見やすい形式でUsageを定義してくれるのは嬉しいです。
# ./argparse_cli.py --help usage: argparse_cli.py [-h] -n NAME [-l LAST_NAME] Simple argparse CLI optional arguments: -h, --help show this help message and exit -n NAME, --name NAME your name -l LAST_NAME, --last-name LAST_NAME your last name
バリデーションエラー
必須オプション(required=True
)のNAMEを省略すると、以下のようにエラーとなります。
# ./argparse_cli.py usage: argparse_cli.py [-h] -n NAME [-l LAST_NAME] argparse_cli.py: error: argument -n/--name is required
正常動作
LAST_NAMEは省略してもdefault
で設定した値が反映されます。
# ./argparse_cli.py -n Shinji Your name is Shinji Fujimoto
Clickの場合
次にClickによる実装です。
Clickはデコレータによる実装となります。デコレータの引数はargparse
と互換性を保たれているので、argparseを利用している方はClickの利用は難しくないことでしょう。
#!/usr/bin/env python import click @click.command(help='Simple click CLI') # (1) @click.option('-n', '--name', 'name', type=str, help='your name', required=True) # (2) @click.option('-l', '--last-name', 'last_name', type=str, help='your last name', default='Fujimoto', required=False) def main(name, last_name): print("Your name is %s" % name) if __name__ == '__main__': main()
click.command
デコレータをメソッドに設定することで、コマンド指定時にコールするメソッドを決定します。-
click.option
デコレータによりオプションを追加します。
usageの表示
ほぼ同じですね。大きな違いはコマンドの説明が[OPTIONS]
で束ねられたことでしょうか。argparse
だと直感的にコマンドを理解できますが、オプションが多くなった時に長くなって見づらくなるので好みの問題でしょうか。
# ./click_cli.py --help Usage: click_cli.py [OPTIONS] Simple click CLI Options: -n, --name TEXT your name [required] -l, --last-name TEXT your last name --help Show this message and exit.
バリデーションエラー
# ./click_cli.py Usage: click_cli.py [OPTIONS] Error: Missing option "-n" / "--name".
正常動作
# ./click_cli.py -n Shinji Your name is Shinji Fujimoto
シンプルな引数を渡すケースであれば、どちらで実装してもよいかな、という印象です。
サブコマンドの実装
次にサブコマンドを実装します。サブコマンドを実装してくるあたりから、argparseは可読性が悪くなるような気がします。今回は2階層のサブコマンドでaws-cliっぽいのを実装します。
argparseの場合
まずはargparseによる実装です。argparse
でサブコマンドを定義する場合、add_subparsers
メソッドにより子parserのグループを生成し、add_parser
メソッドによりサブコマンドを追加します。追加したparserにメソッドを紐付けて処理を記載します。
#!/usr/bin/env python import argparse import boto3 import json def get_args(): parser = argparse.ArgumentParser(description='Subcommand argparse CLI') subparsers = parser.add_subparsers() # (1) ec2_parser = subparsers.add_parser('ec2', help='EC2 API') # (2) s3_parser = subparsers.add_parser('s3', help='S3 API') rds_parser = subparsers.add_parser('rds', help='RDS API') ec2_subparsers = ec2_parser.add_subparsers() # (3) describe_instances_parser = ec2_subparsers.add_parser('describe_instances', help='EC2 DescribeInstances API') describe_instances_parser.add_argument('--instance-id', type=str) describe_instances_parser.set_defaults(func=describe_instances) # (4) run_instances_parser = ec2_subparsers.add_parser('run_instances', help='EC2 RunInstances API') run_instances_parser.set_defaults(func=run_instances) return parser.parse_args() def describe_instances(args): # (4) instance_ids = [args.instance_id] if args.instance_id else [] print(json.dumps(boto3.client('ec2').describe_instances(InstanceIds=instance_ids))) def run_instances(args): : def main(): args = get_args() args.func(args) if __name__ == '__main__': main()
add_subparsers
メソッドにより子parserのグループを作成します。-
add_parser
メソッドによりサブコマンドを作成します。 -
サブコマンドをネストできます。
-
サブコマンドとコールする関数を紐付けます。
usageの表示
各サブコマンド毎に--help
オプションが自動で実装されています。
# ./argparse_awscli.py --help usage: argparse_awscli.py [-h] {ec2,s3,rds} ... Subcommand argparse CLI positional arguments: {ec2,s3,rds} ec2 EC2 API s3 S3 API rds RDS API optional arguments: -h, --help show this help message and exit # ./argparse_awscli.py ec2 --help usage: argparse_awscli.py ec2 [-h] {describe_instances,run_instances} ... positional arguments: {describe_instances,run_instances} describe_instances EC2 DescribeInstances API run_instances EC2 RunInstances API optional arguments: -h, --help show this help message and exit
サブコマンド実行
# ./argparse_awscli.py ec2 describe_instances {"Reservations": [], "ResponseMetadata": {"HTTPStatusCode": 200, "RequestId": "15941b27-087e-4840-94b5-61ee1c3564ff"}}
紐付けた関数が正常に実行されています。
Clickの場合
次にコマンドによる
#!/usr/bin/env python import click import boto3 import json @click.group(help='Subcommand click CLI') # (1) @click.option('-p', '--profile', type=str) @click.pass_context def main(ctx, profile): ctx.params['session'] = boto3.session.Session(profile_name=ctx.params.get('profile')) @main.group(help='EC2 API') # (2) @click.pass_context def ec2(ctx): ctx.params['client'] = ctx.parent.params['session'].client('ec2') @main.group(help='S3 API') @click.pass_context def s3(ctx): ctx.params['client'] = ctx.parent.params['session'].client('s3') @main.group(help='RDS API') @click.pass_context def rds(ctx): ctx.params['client'] = ctx.parent.params['session'].client('rds') @ec2.command(help='EC2 DescribeInstances API') @click.option('--instance-id', type=str, help='specify instance id') @click.pass_context def describe_instances(ctx, instance_id): instance_ids = [instance_id] if instance_id else [] print(json.dumps(ctx.parent.params['client'].describe_instances(InstanceIds=instance_ids))) @ec2.command(help='EC2 RunInstances API') def run_instances(ctx): pass if __name__ == '__main__': main()
click.group
デコレータによりサブコマンドのグループを定義します。argparseと違ったサブコマンドのグループで共通処理を書けます。-
サブコマンドをネストできます。
usageの表示
argparse同様、Clickでも各サブコマンド毎に--help
オプションが自動で実装されています。
./click_awscli.py --help Usage: click_awscli.py [OPTIONS] COMMAND [ARGS]... Subcommand click CLI Options: -p, --profile TEXT --help Show this message and exit. Commands: ec2 EC2 API rds RDS API s3 S3 API # ./click_awscli.py ec2 --help Usage: click_awscli.py ec2 [OPTIONS] COMMAND [ARGS]... EC2 API Options: --help Show this message and exit. Commands: describe_instances EC2 DescribeInstances API run_instances EC2 RunInstances API
サブコマンド実行
# ./click_awscli.py ec2 describe_instances {"Reservations": [], "ResponseMetadata": {"HTTPStatusCode": 200, "RequestId": "8c3b5872-5813-4a91-92f0-e8fc44e89db6"}}
上位のサブコマンドグループの関数から順次実行されています。
まとめ
いかがでしたでしょうか?
Pythonで利用可能なコマンドラインパーサーであるargparse、Clickを簡単にご紹介しました。正直、好みの問題もあると思うので、使い易い方を使うのがベストかと。ソースコードを読んでると色々な便利ライブラリを知ることができるので、