この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
こんにちは、藤本です。
先日エントリした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を簡単にご紹介しました。正直、好みの問題もあると思うので、使い易い方を使うのがベストかと。ソースコードを読んでると色々な便利ライブラリを知ることができるので、