clickではじめる社内コマンドツール -その2-

業務中に繰り返し行う作業を社内コマンドとしてまとめています。
その中で共有できそうな知見がいくつか出てきたのでSENSY Advent Calendar 2018で数回に分けて紹介していきます。
今回は、サブコマンドの作り方を紹介します。

* 第一回目: [CLIツール作成のために利用しているPython製ライブラリのclickを紹介](https://qiita.com/rhoboro/items/7812f3bea759fa7a091f)

## サブコマンド

この記事でサブコマンドとは、`git clone`や`git checkout`のようなあるコマンド（`git`）の配下にさらに別のコマンド（`clone`や`chekcout`）を用意すること言います。clickのドキュメントでは、[Nesting Commands](https://click.palletsprojects.com/en/7.x/quickstart/#nesting-commands)という名前がついているようです。

サブコマンドをclickで実現するためには、`Group`という複数のコマンドをひとまとめにできる機能を使います。

### Groupを使ってコマンドをまとめる

次の内容で`service.py`を用意してください。

```python
import click

@click.group()
def service():
    pass

@service.command()
@click.option('--name', '-n', required=True)
def create(name):
    click.echo(f'create {name}')

@service.command()
def list():
    click.echo('show list!!')


if __name__ == '__main__':
    service()
```

ここで軽くコードの説明をします。

まず、3行目の`@click.group()`でサブコマンドを集約するためのグループとして関数service()を登録しています。
つづいて、7,11,15行目の`@service.command()`でserviceコマンドのサブコマンドになる処理を登録しています。

### サブコマンドの動作を確認

ここまで書けたら、[前回](https://qiita.com/rhoboro/items/7812f3bea759fa7a091f)と同じようにsetup.pyを用意してください。

```python
from setuptools import setup

setup(
    name='service',
    version='0.1',
    py_modules=['service'],
    install_requires=[
        'Click',
    ],
    entry_points='''
        [console_scripts]
        service=service:service  # serviceコマンドにservice.pyの関数service()を紐づける
    ''',
)
```

インストールしてコマンドを実行すると、サブコマンドが作成されていることがわかります。
サブコマンドによって実行内容が切り替わっていますね。

```sh
(venv) $ pip install -e .
(venv) $ service create --name newservice
create newservice
(venv) $ service list
show list!!
```

## サブサブコマンド

Gitのような単一のサービスのコマンドだけであればサブコマンドを利用できれば十分かもしれません。

しかし、社内向けのコマンドツールとしては、社内で利用されている様々なコマンドを体系的にまとめあげたいところです。そこで、`aws s3 ls`や`gcloud app deploy`を参考に`<コマンド> <サービス> <操作>`というコマンド体系にしていきます。

これはを実現するには下記の2パターンが考えられます。

* サブコマンドの最初のオプションで処理を切り替える
* サブコマンドをネストする

今回は後者の「サブコマンドをネストする」パターンを紹介します。

### サブコマンドをネストする

同じディレクトリに次の内容で`company.py`を用意してください。

```python
import click
from service import service

@click.group()
def company():
    pass

# グループのサブコマンドにグループを登録
company.add_command(service)

if __name__ == '__main__':
    company()
```

先程と同様にcompanyというグループを作成しました。
そして、そこに`company.add_command(service)`で先程作ったグループのserviceを登録しています。
サブコマンドのネストはこれで完成です。

### サブサブコマンドの動作を確認

それでは動作を確認してみましょう。
setup.pyを変更し、エントリポイントがcompany.pyから始まるようにします。

```diff
  from setuptools import setup
  
  setup(
--    name='service',
++    name='company',
      version='0.1',
--    py_modules=[, 'service'],
++    py_modules=['company', 'service'],
      install_requires=[
          'Click',
      ],
      entry_points='''
          [console_scripts]
--        service=service:service
++        company=company:company
      ''',
  )
```

先ほどインストールしていたserviceコマンドはもう不要なので先にアンインストールしてます。
その後、companyコマンドをインストールし動作を確認してみましょう。

```sh
# 不要になったserviceコマンドをアンインストール
(venv) $ pip uninstall service
(venv) $ pip install -e .
# 引数なしだとデフォルトでヘルプが表示される。Commandsにサブコマンドserviceが登録されている
(venv) $ company
Usage: company [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  service
# サブサブコマンドの実行
(venv) $ company service create --name newservice
create newservice
(venv) $ company service list
show list!!
```

意図した通りに動きました！！

## まとめ

今回はclickでサブコマンドを登録方法を紹介しました。また、それをネストさせてみました。
これだけのコードで`aws s3 ls`のような階層構造のコマンド体系を簡単に用意できます。
サブコマンドは、複数のコマンドをまとめ上げたいときは非常に便利なのでぜひ使ってみてください。