Python コマンドライン解析ライブラリの比較
はじめに
この記事は Python Advent Calender 18 日目の記事です。
最近になって少しコマンドラインの使用を d することが増えてきました。デフォルトである argparse を使ってきましたが、あまり覚えられないこともあり他の選択肢になるがあるかを調べ比較していきます。
人気がありそうな気がした click,fire と調査・比較していきます。
この記事では下記のバーションのライブラリを使用します。
pipenv --python 3.7.5
$ pipenv install click==7.0
$ pipenv install fire==0.2.1
作成するコマンドラインツール
python commands.py [command] [options] NAME
基本
$ python commands.py hello World
Hello, World
$ python commands.py hellow World
Goodbye, World
オプション
$ python commands.py hello --greeting=Wazzup World
Whazzup, World
$ python commands.py goodbye --greeting=Later World
Later, World
$ python commands.py hello --caps World
HELLO, WORLD
$ python commands.py hello --greeting=Wazzup --caps World
WAZZUP, WORLD
この記事では、参考記事ある流れを沿って以下の機能を実装するための各ライブラリーの方式を比較します。
- コマンド (hello, goodbye)
- 引数
- オプション/フラグ (--greeting=<str>, --caps)
追加機能(あとで書きます)
- バージョン表示
- ヘルプメッセージ
- エラー処理
コマンド
各種ライブラリの基本的な書き方をやっていきます。
Argparse
import argparse
def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
hello_parser = subparsers.add_parser("hello")
goodbye_parser = subparsers.add_parser("goodbye")
parser.parse_args()
if __name__ == "__main__":
main()
これで 2 つのコマンドと組込みのヘルプメッセージができました。コマンド hello のオプションとして実行するとヘルプメッセージが変わります。
$ python argparse/commands.py --helop
usage: commands.py [-h] {hello} ...
positional arguments:
{hello}
optional arguments:
-h, --help show this help message and exit
$ python commands.py hello --help
usage: commands.py hello [-h]
optional arguments:
-h, --help show this help message and exit
Click
import click
@click.group()
def greet():
pass
@greet.command()
def hello(**kwargs):
pass
@greet.command()
def goodbye(**kwargs):
pass
if __name__ == "__main__":
greet()
これで 2 つのコマンドと組込みのヘルプメッセージができました。コマンド hello のオプションとして実行するとヘルプメッセージが変わります。
$ python click/commands.py --help
Usage: commands.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
goodbye
hello
$ python click/commands.py hello --help
Usage: commands.py hello [OPTIONS]
Options:
--help Show this message and exit.
Fire
import fire
def hello():
pass
def goodbye():
pass
if __name__ == "__main__":
fire.Fire({"hello": hello, "goodbye": goodbye})
これで 2 つのコマンドと組込みのヘルプメッセージができました。コマンド hello のオプションとして実行するとヘルプメッセージが変わります。また、fire は man 形式でヘルプが表示されます。また fire は下記のような方法でも実装ができます。
- 関数を並べる方法
import fire
def hello():
pass
def goodbye():
pass
if __name__ == "__main__":
fire.Fire()
- クラスで渡す方法
import fire
class Greet:
def hello(self):
pass
def goodbye(self):
pass
if __name__ == "__main__":
greet = Greet()
fire.Fire(greet)
$ python fire/commands.py --help
NAME
commands.py
SYNOPSIS
commands.py COMMAND
COMMANDS
COMMAND is one of the following:
goodbye
hello
$ python fire/commands.py hello --help
NAME
commands.py hello
SYNOPSIS
commands.py hello
基本的なものでのライブラリごとに異なるアプローチで非常におもしろいですね。次は NAME 引数と各ツールからの結果出力をするロジックを追加していきます。
引数
ここでは、上で書いたコードに新しいロジックを追加します。目的を示すコメントを新しい行に追加します。引数(位置指定)は必須入力です。今回は、name
でツールが特定の人に挨拶できるように引数を追加します。
Argparse
サブコマンドに引数を追加するには、add_argument
を使用します。そして、関数を実行するためにset_defautls
で設定します。最後に、引数を解析した後にargs.func(args)
で関数を実行します。
import argparse
def hello(args):
print(f"Hello, {args.name}")
def goodbye(args):
print(f"Goodbye, {args.name}")
def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
hello_parser = subparsers.add_parser("hello")
hello_parser.add_argument("name")
hello_parser.set_defaults(func=hello)
goodbye_parser = subparsers.add_parser("goodbye")
goodbye_parser.add_argument("name")
goodbye_parser.set_defaults(func=goodbye)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
$ python argparse/commands.py hello World
Hello, World
$ python argparse/commands.py hello --help
usage: commands.py hello [-h] name
positional arguments:
name
optional arguments:
-h, --help show this help message and
Click
Click に引数を追加するには、@click.argument
を使用します。この場合は引数名を渡すだけですが、色んなのオプションが存在します。
$ python click/commands.py hello World
Hello, World
$ python click/commands.py hello --help
Usage: commands.py hello [OPTIONS] NAME
Options:
--help Show this message and exit.
Fire
Fire では関数に引数を追加するだけになります。Fire では基本的には普通に関数・クラスを実装するだけになるのでかなりシンプルな形になりました。
import fire
def hello(name):
print(f"Hello, {name}")
def goodbye(name):
print(f"Goodbye, {name}")
if __name__ == "__main__":
fire.Fire({"hello": hello, "goodbye": goodbye})
$ python fire/commands.py hello World
Hello, World
(test-cli) ikura4@ikura1-ThinkPad:~/test/test-cli$ python fire/commands.py hello --help
NAME
commands.py hello
SYNOPSIS
commands.py hello NAME
POSITIONAL ARGUMENTS
NAME
NOTES
You can also use flags syntax for POSITIONAL ARGUMENTS
(END)
オプション/フラグ
ここでは、上で書いたコードに新しいロジックを再度追加します。オプションは必須ではない指定できる入力になります。
この例では、--greeting=[greeting]
と--caps
を追加します。greeting
にはデフォルト値はHello
かGoodbye
になり、ユーザーは任意の値を渡すことができます。例えば、--greeting=Wazzup
とすると、Wazzup, [name]
と表示されます。--caps
を与えられた場合、表示が全てになります。例えば、--caps
とすると、HELLO [NAME]
と表示されます。
Argparse
import argparse
# greetingを渡すようになったので
# greet関数に統一
def greet(args):
output = f"{args.greeting}, {args.name}"
if args.caps:
output = output.upper()
print(output)
def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
hello_parser = subparsers.add_parser("hello")
# 引数にnameを追加します
hello_parser.add_argument("name")
# greetingオプションとデフォルトを追加
hello_parser.add_argument("--greeting", default="Hello")
# flagをデフォルトFalseで追加
hello_parser.add_argument("--caps", action="store_true")
hello_parser.set_defaults(func=greet)
goodbye_parser = subparsers.add_parser("goodbye")
goodbye_parser.add_argument("name")
goodbye_parser.add_argument("--greeting", default="Goodbye")
goodbye_parser.add_argument("--caps", action="store_true")
goodbye_parser.set_defaults(func=greet)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
$ python argparse/commands.py hello --greeting=Wazzup World
Wazzup, World
$ python argparse/commands.py hello --caps World
HELLO, WORLD
$ python argparse/commands.py hello --greeting=Wazzup --caps World
WAZZUP, WORLD
$ python argparse/commands.py hello --help
usage: commands.py hello \[-h\] [--greeting GREETING] [--caps] name
positional arguments:
name
optional arguments:
-h, --help show this help message and exit
--greeting GREETING
--caps
Click
@click.option
でgreeting
とcaps
を追加します。デフォルト値があるので関数を一つにしました。
import click
def greeter(name, greeting, caps):
output = f"{greeting}, {name}"
if caps:
output = output.upper()
print(output)
@click.group()
def greet():
pass
@greet.command()
@click.argument("name")
# greetingオプションとデフォルトを追加
@click.option("--greeting", default="Hello")
# flagを追加(is_flag=Trueでフラグにできる)
@click.option("--caps", is_flag=True)
def hello(name, greeting, caps):
greeter(name, greeting, caps)
@greet.command()
@click.argument("name")
@click.option("--greeting", default="Goodbye")
@click.option("--caps", is_flag=True)
def goodbye(name, greeting, caps):
greeter(name, greeting, caps)
if __name__ == "__main__":
greet()
$ python click/commands.py hello --greeting=Wazzup World
Wazzup, World
$ python click/commands.py hello --caps World
HELLO, WORLD
$ python click/commands.py hello --greeting=Wazzup --caps World
WAZZUP, WORLD
$ python click/commands.py hello --helpUsage: commands.py hello [OPTIONS] NAME
Options:
--greeting TEXT
--caps
--help Show this message and exit.
Fire
関数に引数を追加することでgreeting
とcaps
を追加します。また共通処理があるのでgreet
にまたまとめました。
import fire
def greet(name, greeting, caps):
output = f"{greeting}, {name}"
if caps:
output = output.upper()
print(output)
def hello(name, greeting="Hello", caps=False):
greet(name, greeting, caps)
def goodbye(name, greeting="Goodbye", caps=False):
greet(name, greeting, caps)
if __name__ == "__main__":
fire.Fire({"hello": hello, "goodbye": goodbye})
fire には bool を渡す時には注意する点があります。--caps
を渡した後ろのトークンは--caps に渡されます。
指定する方法としては以下の 3 つがあります。
- 一番後ろに bool オプションを付ける
$ python fire/commands.py hello World --caps
- スペース区切りで値を渡す
$ python fire/commands.py hello --caps True World
- =区切りで値を渡す
$ python fire/commands.py hello --caps=True World
$ python fire/commands.py hello --greeting=Wazzup World
Wazzup, World
$ python fire/commands.py hello --caps=True World
HELLO, WORLD
$ python fire/commands.py hello --greeting=Wazzup --caps=True World
WAZZUP, WORLD
$ python fire/commands.py hello --help
NAME
commands.py hello
SYNOPSIS
commands.py hello NAME <flags>
POSITIONAL ARGUMENTS
NAME
FLAGS
--greeting=GREETING
--caps=CAPS
NOTES
You can also use flags syntax for POSITIONAL ARGUMENTS
(END)
最後に
-
Argparser
- 標準ライブラリという魅力があり、他方で使用されていて良いものなのですが、簡単な CLI を作るときには向いていない印象があります。
-
Click
- デコレータは簡易なことをするには、コードが小さくまとまって好きなのですが、
black
のコードなどを見に行くとデコレータが多過ぎて可読性が落る形になっているので小規模に向いている印象を受けています。
- デコレータは簡易なことをするには、コードが小さくまとまって好きなのですが、
- Fire
- Python の基本構文と Fire の簡単な動作さえ学べが簡単に CLI が作れてしまうお手軽ライブラリです。ですが、細かい設定はできないので凝ったことをすると困るかもしれません。
私が作るものは基本的に小規模なものが多いので、Fireを使用していきます。次点でClickを考えています。
参考ページ
Comparing Python Command-Line Parsing Libraries – Argparse, Docopt, and Click