Pythonコマンドライン解析ライブラリの比較(argparse, click, fire)

# Python コマンドライン解析ライブラリの比較

## はじめに

この記事は Python Advent Calender 18 日目の記事です。

最近になって少しコマンドラインの使用を d することが増えてきました。デフォルトである argparse を使ってきましたが、あまり覚えられないこともあり他の選択肢になるがあるかを調べ比較していきます。
人気がありそうな気がした [click](https://github.com/pallets/click),[fire](https://github.com/google/python-fire) と調査・比較していきます。

この記事では下記のバーションのライブラリを使用します。

```sh
pipenv --python 3.7.5
$ pipenv install click==7.0
$ pipenv install fire==0.2.1
```

## 作成するコマンドラインツール

```sh
python commands.py [command] [options] NAME
```

### 基本

```sh
$ python commands.py hello World
Hello, World
$ python commands.py hellow World
Goodbye, World
```

### オプション

```sh
$ 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
```

この記事では、参考記事ある流れを沿って以下の機能を実装するための各ライブラリーの方式を比較します。

1. コマンド (hello, goodbye)
2. 引数
3. オプション/フラグ (--greeting=\<str>, --caps)

追加機能(あとで書きます)

1. バージョン表示
2. ヘルプメッセージ
3. エラー処理

## コマンド

各種ライブラリの基本的な書き方をやっていきます。

### Argparse

```python
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 のオプションとして実行するとヘルプメッセージが変わります。

```sh
$ 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

```python
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 のオプションとして実行するとヘルプメッセージが変わります。

```shell
$ 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

```python
import fire

def hello():
    pass

def goodbye():
    pass

if __name__ == "__main__":
    fire.Fire({"hello": hello, "goodbye": goodbye})
```

これで 2 つのコマンドと組込みのヘルプメッセージができました。コマンド hello のオプションとして実行するとヘルプメッセージが変わります。また、fire は man 形式でヘルプが表示されます。また fire は下記のような方法でも実装ができます。

- 関数を並べる方法

```python
import fire

def hello():
    pass

def goodbye():
    pass

if __name__ == "__main__":
    fire.Fire()
```

- クラスで渡す方法

```python
import fire

class Greet:
    def hello(self):
        pass

    def goodbye(self):
        pass

if __name__ == "__main__":
    greet = Greet()
    fire.Fire(greet)
```

```sh
$ 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)`で関数を実行します。

```python
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()
```

```sh
$ 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`を使用します。この場合は引数名を渡すだけですが、色んなの[オプション](http://click.pocoo.org/5/arguments/)が存在します。

```sh
$ 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 では基本的には普通に関数・クラスを実装するだけになるのでかなりシンプルな形になりました。

```python
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})
```

```sh
$ 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

```python
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()
```

```sh
$ 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`を追加します。デフォルト値があるので関数を一つにしました。

```python
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()
```

```sh
$ 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`にまたまとめました。

```python
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`

```sh
$ 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](https://realpython.com/comparing-python-command-line-parsing-libraries-argparse-docopt-click/#command-line-example)