読者です 読者をやめる 読者になる 読者になる

CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: コマンドラインパーサの Click が便利すぎた

Python

Python のコマンドラインパーサといえば、標準ライブラリに組み込まれている argparse が基本。 蛇足になるけど、バージョン 2.7 以前で使われていた optparse は将来的に廃止予定なので新たに使うことは避けた方が良い。

そして、今回紹介する Python のサードパーティ製コマンドラインパーサ Click は、既存のパッケージと比較すると最小限のコードで美しくコマンドラインインターフェースを実装できるように作られている。 どれくらい楽になるかといえば、もう argparse を使っている場合じゃないな、と思えるレベル。

Welcome to the Click Documentation — Click Documentation (5.0)

Click をインストールする

まずは Click を PyPI からインストールしよう。

$ pip install click
$ pip list | grep click
click (5.1)

基本的な使い方

まずは使い方の雰囲気から見ていこう。

コマンドを定義する

Click を使ってコマンドを定義するには、@click.command() デコレータを使って関数を修飾するだけでいい。 これで Click にコマンドが登録される。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
def cmd():
    click.echo('Hello, World!')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると cmd() 関数の内容が実行される。 とはいえ、これだけだと Click を使っていようと使っていまいと変わらない。

$ python helloworld.py
Hello, World!

オプションを追加する

次に、コマンドに対してオプションを追加してみる。 オプションを追加するには @click.option() デコレータを使ってコマンドになる関数を修飾する。 以下では --name/-n オプションをデフォルト値 'World' で登録している。 オプションが指定された場合にはコマンドの引数 name に、指定された内容が代入される。 尚、デフォルトが無い状態でオプションが指定されないと変数は None になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--name', '-n', default='World')
def cmd(name):
    msg = 'Hello, {name}!'.format(name=name)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると --name/-n オプションが認識されていることがわかる。

$ python option.py
Hello, World!
$ python option.py --name Click
Hello, Click!
$ python option.py -n Sekai
Hello, Sekai!

引数を追加する

先ほど追加したのはオプションなので、ユーザが指定しないことも許容するものだった。 次は必ず指定が必要な引数を追加してみる。 もう Click の API 設計は雰囲気がつかめたと思うけど、ここでもデコレータを使うことになる。 引数を追加するには @click.argument() デコレータを使えばいい。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('name')
def cmd(name):
    msg = 'Hello, {name}!'.format(name=name)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行してみる。 引数はユーザが指定しないとエラーになる。

$ python argument.py
Usage: argument.py [OPTIONS] NAME

Error: Missing argument "name".
$ python argument1.py World
Hello, World!

サブコマンドを作る

ひとつのコマンドで複数の操作をサポートする場合には、コマンドの中に操作別でサブコマンドを追加することがある。 もちろん Click はサブコマンドを作ることもできる。 以下のように、まずは @click.group() デコレータでエントリポイントとなる関数を修飾する。 そして、次は修飾した関数自体がデコレータとしてサブコマンドを登録できるようになる。 ここらへんを見ると API の設計に Flask の影響を受けている感じ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


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


@cmd.command()
def english():
    click.echo('Hello, World!')


@cmd.command()
def japanese():
    click.echo('Konnichiwa, Sekai!')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行するとサブコマンド english と japanese が作られていることがわかる。

$ python subcommand.py
Usage: subcommand.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  english
  japanese
$ python subcommand.py english
Hello, World!
$ python subcommand.py japanese
Konnichiwa, Sekai!
$ python subcommand.py french
Usage: subcommand.py [OPTIONS] COMMAND [ARGS]...

オプションについて詳しく見ていく

ここからはオプションで指定できるパラメータなどを見ていくことにする。

オプションに説明を追加する

オプションに説明を追加するには help パラメータを指定すればいい。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--prefix', default='Hello', help='Prefix of greetings.')
def cmd(prefix):
    msg = '{prefix}, World!'.format(prefix=prefix)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

これでコマンドに --help オプションを指定した際に説明が出力されるようになる。

$ python help.py --help
Usage: help.py [OPTIONS]

Options:
  --prefix TEXT  Prefix of greetings.
  --help         Show this message and exit.
$ python help.py --prefix Konnichiwa
Konnichiwa, World!

オプションで受け取る型を明示する

ここまでのオプションで受け取っていたのはすべて文字列だけど、type パラメータに型を指定すると引数がその型で受け取れるようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=int, help='Your age.')
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行してみる。 オプションの型に int (整数型) を指定したので、そこに文字列を入れようとするとエラーになることがわかる。

$ python type.py --age Hello,World!
Usage: type.py [OPTIONS]

Error: Invalid value for "--age": Hello,World! is not a valid integer
$ python type.py --age 100
Your age: 100

オプションの内容を検証 (バリデーション) する

先ほどの例では型は指定したものの、値の範囲などを検証していなかった。 次のサンプルコードではオプションで受け取った引数を検証している。 検証した結果、不正な値が入っていることがわかった場合には click.BadParameter() 例外を raise すれば良い。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=int, help='Your age.')
def cmd(age):
    if age < 1:
        raise click.BadParameter('You are liar!')
    if age > 100:
        raise click.BadParameter('Really?!')

    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

今度は、同じ整数でもおかしな値を入力するとエラーになる。

$ python badparam.py --age -1
Usage: badparam.py [OPTIONS]

Error: Invalid value: You are liar!
$ python badparam.py --age 101
Usage: badparam.py [OPTIONS]

Error: Invalid value: Really?!

コールバック関数で値を検証する

先ほどのコードではコマンドとなる関数自体に値を検証するコードが含まれていた。 しかし、これは可読性の上であまり好ましいとはいえないだろう。 以下のように、オプションの callback パラメータでコールバック関数を登録しておき、その中で値の検証をするとコードの見通しがよくなるかもしれない。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


def validate_age(ctx, param, value):
    age = value

    if age < 1:
        raise click.BadParameter('You are liar!')
    if age > 100:
        raise click.BadParameter('Really?!')


@click.command()
@click.option('--age', callback=validate_age, default=18)
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記も、先ほどと同様におかしな値を入れるとエラーになる。

$ python validate.py --age -1
Usage: validate.py [OPTIONS]

Error: Invalid value for "--age": You are liar!
$ python validate.py --age 101
Usage: validate.py [OPTIONS]

Error: Invalid value for "--age": Really?!

値の範囲を指定する

先ほどは自分で検証用のコードを書いたけど、整数型であれば組み込みで範囲の検証が可能になっている。 type パラメータに click.IntRange() を指定すると、指定した値の範囲で検証してくれる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=click.IntRange(0, 100))
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

エラーメッセージにも具体的な指定できる値の範囲が出力されていて親切な感じ。

$ python intrange.py --age 101
Usage: intrange.py [OPTIONS]

Error: Invalid value for "--age": 101 is not in the valid range of 0 to 100.
$ python intrange.py --age -1
Usage: intrange.py [OPTIONS]

Error: Invalid value for "--age": -1 is not in the valid range of 0 to 100.

ちなみに click.IntRange() は clamp パラメータを True に指定すると、範囲を超えた内容が入力されたときに範囲内の近い値に補正してくれる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--age', type=click.IntRange(0, 100, clamp=True))
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

clamp パラメータを指定することで 101 は 100 に、-1 は 0 に補正された。

$ python intrange.py --age 101
Your age: 100
$ python intrange.py --age -1
Your age: 0

オプションを選択肢から選べるようにする

これまでの例ではオプションはユーザの自由入力だった。 それに対して type パラメータに click.Choice() を指定すると、既存の選択肢から選ぶ形になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--language', type=click.Choice(['Japanese', 'English']))
def cmd(language):
    click.echo(language)


def main():
    cmd()


if __name__ == '__main__':
    main()

実行すると、選択肢の中にある Japanese や English は指定できるが、例えば存在しない French を指定するとエラーになる。

$ python choice.py --help
Usage: choice.py [OPTIONS]

Options:
  --language [Japanese|English]
  --help                         Show this message and exit.
$ python choice.py --language Japanese
Japanese
$ python choice.py --language French
Usage: choice.py [OPTIONS]

Error: Invalid value for "--language": invalid choice: French. (choose from Japanese, English)

タプルで引数を受け取る

ひとつのオプションで複数の引数を受け取りたい場合には、type パラメータにタプルを指定できる。 こうすればタプル内のそれぞれの型でオプションを受け取ることができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--values', type=(str, int))
def cmd(values):
    msg = '{s} {i}'.format(s=values[0], i=values[1])
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記であれば --values オプションひとつに文字列と整数のふたつを渡すことになる。

$ python tuplevalues.py --values Hello 123
Hello 123

同じ名前で複数のオプションを受け取れるようにする

今度は同じ名前のオプションを複数回使えるようにするパターン。 これには multiple パラメータを True にすれば良い。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--values', multiple=True)
def cmd(values):
    for value in values:
        click.echo(value)


def main():
    cmd()


if __name__ == '__main__':
    main()

これで複数回 --values オプションが指定できるようになる。

$ python multiopts.py --values 1 --values 2
1
2

オプションの数をカウントする

コマンドラインインターフェースでよくあるパターンとして -v/--verbose オプションの個数で出力される内容の詳しさが変化するというものがある。 これを実現するには count パラメータを True に指定する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('-v', '--verbose', count=True)
def cmd(verbose):
    msg = 'Verbosity: {v}'.format(v=verbose)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

これでオプションが指定された個数が手に入る。

$ python count.py -vvvv
Verbosity: 4
$ python count.py -v -v -v
Verbosity: 3
$ python count.py -v --verbose
Verbosity: 2

オプションで真偽値のフラグを扱う

真偽値を扱う場合には is_flag オプションを True にする。 これで、オプションの有無に応じて引数に真偽値が代入されるようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--shout', is_flag=True)
def cmd(shout):
    msg = 'Hello, World!'
    if shout:
        msg = msg.upper()
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると --shout オプションを指定した場合のみメッセージが大文字になる。

$ python boolflag.py
Hello, World!
$ python boolflag.py --shout
HELLO, WORLD!

また、'/' 記号を挟んでふたつオプションを指定した上でデフォルト値を真偽値に設定するやり方もある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--shout/--no-shout', default=False)
def cmd(shout):
    msg = 'Hello, World!'
    if shout:
        msg = msg.upper()
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

この場合は、どちらか片方のオプションのみが有効なものとして動作する。

$ python boolflag.py
Hello, World!
$ python boolflag.py --shout
HELLO, WORLD!
$ python boolflag.py --no-shout
Hello, World!

異なる名前のオプションでひとつの引数を扱う

以下のようにすると、異なる名前のオプションがひとつの引数に対して値を代入するようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--upper', 'transformation', flag_value='upper')
@click.option('--lower', 'transformation', flag_value='lower', default=True)
def cmd(transformation):
    click.echo(transformation)


def main():
    cmd()


if __name__ == '__main__':
    main()

--upper オプションを指定した場合には transformation 引数に 'upper' が、反対に --lower オプションを指定した場合には 'lower' が代入される。

$ python switch.py
lower
$ python switch.py --upper
upper
$ python switch.py --lower
lower

動的にオプションのデフォルト値を決定する

オプションのデフォルト値には静的な値以外にも動的な値を指定することもできる。 例えば、以下のように default パラメータに値を返す関数を指定しておくと、動的にデフォルト値を決定できる。 以下では 1 ~ 100 の範囲でデフォルト値がランダムに代入される。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import random

import click


@click.command()
@click.option('--age', default=lambda: random.randint(1, 100))
def cmd(age):
    msg = 'Your age: {age}'.format(age=age)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

実行すると、確かに毎回表示される値が異なる。

$ python dynamicdefault.py
Your age: 98
$ python dynamicdefault.py
Your age: 63
$ python dynamicdefault.py
Your age: 41

オプションのデフォルト値を環境変数から取得する

オプションのデフォルト値を環境変数にするというのも、コマンドラインインターフェースでは多用されるパターンだとおもう。 Click でそれを実現するには envvar パラメータに環境変数名を指定するだけでいい。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--shell', envvar='SHELL')
def cmd(shell):
    click.echo(shell)


def main():
    cmd()


if __name__ == '__main__':
    main()

オプションが指定されていないときは環境変数から、指定されたときはその値が使用される。

$ python env.py
/bin/zsh
$ python env.py --shell /bin/bash
/bin/bash

引数について詳しく見ていく

ここからは引数 (argument) について詳しく見ていくことにする。 ちなみに引数で紹介したパラメータでも、実際にはオプションで使えたりするものは多い。

複数の引数を指定させる

引数の個数は nargs パラメータで指定できる。 その中にはひとつだけ -1 を指定することができて、これはその引数が何個でも指定できることを意味している。 以下では src 引数が幾つでも指定できた上で、dst 引数がひとつだけ指定できる。

#!/usr/bin/intrange python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('src', nargs=-1)
@click.argument('dst', nargs=1)
def cmd(src, dst):
    for i in src:
        msg = 'move from {src} to {dst}'.format(src=i, dst=dst)
        click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行した結果は次の通り。

$ python nargs.py src1 src2 src3 dst
move from src1 to dst
move from src2 to dst
move from src3 to dst

引数でファイルを扱う

type パラメータに click.File() オプションを指定するとオープン済みのファイルオブジェクトが代入されることになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('file', type=click.File('r'))
def cmd(file):
    with file as f:
        while True:
            line = f.readline()
            if not line:
                break
            click.echo(line, nl=False)


def main():
    cmd()


if __name__ == '__main__':
    main()

試しにテキストファイルを用意して指定すると、その内容が取得できていることがわかる。

$ cat << EOF > greet.txt
Hello,
World!
EOF
$ python file.py greet.txt
Hello,
World!

パス文字列を扱う

先ほどの click.File() ではオープン済みのファイルオブジェクトが取得できた。 ただ、ファイルを開くのはそのファイルについて事前に色々とチェックしてから、ということも多いはず。 そんなときは click.Path() を指定する。 その際 exists パラメータを True に指定すると、ファイルの有無も同時にチェックしてくれる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.argument('path', type=click.Path(exists=True))
def cmd(path):
    click.echo(path)
    filename = click.format_filename(path, shorten=True)
    click.echo(filename)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると、指定したパス文字列が存在するファイルであればそのまま得られ、存在しない場合にはエラーになる。

$ python filepath.py /dev/random
/dev/random
random
$ python filepath.py /dev/foo
Usage: filepath.py [OPTIONS] PATH

Error: Invalid value for "path": Path "/dev/foo" does not exist.

インタラクティブな入力を受け付ける方法について詳しく見ていく

Click はコマンドラインのオプションをパースする以外にも、ユーザからインタラクティブに入力を受け付ける方法の提供も行っている。

プロンプトで入力を受け付ける。

オプションを追加する際に prompt パラメータを指定すると、コマンドラインでオプションを指定しなかった際にプロンプトを表示して入力を受け付けることができる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--name', prompt='Name', default='World',
              help='The person to greet.')
def cmd(name):
    msg = 'Hello, {name}!'.format(name=name)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

次のように、オプションの指定がない場合にはプロンプトを表示して入力を受け付け、指定された場合にはそれが使われるようになる。

$ python prompt.py
Name [World]:
Hello, World!
$ python prompt.py
Name [World]: Sekai
Hello, Sekai!
$ python prompt.py --name Click
Hello, Click!

パスワードの入力を受け付ける

オプションを追加する際に prompt=True hide_input=True を指定することで、入力がエコーバックされることがなくなる。 また、confirmation_prompt=True も指定することで、入力を二回促して両者が一致する場合のみ処理を継続するというパスワードを設定させるのに都合のいい挙動になる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
@click.option('--password', prompt=True, hide_input=True,
              confirmation_prompt=True, help='Your password')
def cmd(password):
    msg = 'Your password: {password}'.format(password=password)
    click.echo(msg)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行するとエコーバックのない入力が二回促されて、両者が一致する場合だけ処理が継続する。 もちろん、本当のパスワードであったら標準出力に出してはだめ。

$ python password.py
Password:
Repeat for confirmation:
Your password: password123

コマンドの中でプロンプトを指定する

click.prompt() はデコレータ以外にも、通常の関数として呼び出すこともできる。 なので、次のようにしてコマンドの処理の途中に入力を促すことも可能。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
def cmd():
    age = click.prompt('Please enter your age', type=int)
    click.echo(age)


def main():
    cmd()


if __name__ == '__main__':
    main()

上記の実行結果は次の通り。

$ python rawprompt.py
Please enter your age: 100
100

ユーザに確認 (confirm) を出す

副作用のある処理の前などで、継続するかユーザの指示を仰ぐには click.confirm() を使う。 返り値は y/N が True/False で返ってくるため、そのまま条件分岐に突っ込めばいい。 処理を中断した場合には click.Abort() 例外を raise するのもいいとおもう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.command()
def cmd():
    if not click.confirm('Do you want to continue?'):
        raise click.Abort()

    click.echo('Hello, World!')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると、y を指定したときだけ後続の処理が実行される。

$ python confirm.py
Do you want to continue? [y/N]:
Aborted!
$ python confirm.py
Do you want to continue? [y/N]: y
Hello, World!

サブコマンドについて詳しく見ていく

ここからはサブコマンドを作る場合に指定できるオプションや構造について詳しく見ていくことにする。

サブコマンドからグループコマンドのオプションや引数を参照する

サブコマンドからグループコマンドで指定されたオプションなどを参照する場合には、ちょっと面倒だけど次のようにする。 まず、@click.pass_context デコレータを使ってコンテキストを各コマンドを表した関数のシグネチャに追加する。 コンテキストは各コマンドの間で共有されるオブジェクトなので、そこに辞書型のデータなどを登録しておくことで情報を共有できる。 以下のサンプルコードでいえば、obj という辞書型のデータをコンテキストに追加して、そこの 'DEBUG' というキーでオプションの情報を受け渡ししている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group()
@click.option('--debug', is_flag=True)
@click.pass_context  # コンテキストをインジェクトする
def cmd(ctx, debug):
    # インジェクトしたコンテキストに情報を詰める
    ctx.obj['DEBUG'] = debug


@cmd.command()
@click.pass_context
def subcmd(ctx):
    if ctx.obj['DEBUG']:
        # サブコマンドではコンテキストから情報を取り出す
        click.echo('DEBUG MODE!')
    click.echo('Hello, World!')


def main():
    # コンテキストから参照するアトリビュートを渡す
    cmd(obj={})


if __name__ == '__main__':
    main()

上記を実行すると、サブコマンドからも --debug オプションの情報が参照できていることがわかる。

$ python passcontext.py subcmd
Hello, World!
$ python passcontext.py --debug subcmd
DEBUG MODE!
Hello, World!

サブコマンドを追加した状態でグループコマンドも実行できるようにする

@click.group() デコレータのパラメータに invoke_without_command=True を指定すると、サブコマンドを追加した状態でもグループコマンドが単体で実行できるようになる。 尚、そのままだとサブコマンドが実行された際にグループコマンドを表す関数の内容も一緒に実行されてしまうので、コンテキストから invoked_subcommand メンバの値が存在しない (= サブコマンドが指定されていない) 場合のみグループコマンドの中身を実行するようにしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group(invoke_without_command=True)
@click.pass_context
def cmd(ctx):
    if ctx.invoked_subcommand is None:
        click.echo('This is parent!')


@cmd.command()
def subcmd():
    click.echo('This is child!')


def main():
    # コンテキストから参照するアトリビュートを渡す
    cmd(obj={})


if __name__ == '__main__':
    main()

上記を実行すると、サブコマンドとは別にグループコマンドも実行できるようになっている。

$ python withoutsubcmd.py subcmd
This is child!
$ python withoutsubcmd.py
This is parent!

サブコマンドを複数指定できるようにする

@click.group() デコレータのパラメータ chain に True を指定すると、サブコマンドを複数つなげて (チェイン) 実行できるようにすることができる。 例えば次のサンプルコードではサブコマンド subcmd1 と subcmd2 があるが、どちらも同時に使えるようになる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group(chain=True)
def cmd():
    pass


@cmd.command()
def subcmd1():
    click.echo('subcmd1')


@cmd.command()
def subcmd2():
    click.echo('subcmd2')


def main():
    cmd()


if __name__ == '__main__':
    main()

上記にふたつのサブコマンドを指定して実行した結果が次の通り。

$ python commandchain.py subcmd1 subcmd2
subcmd1
subcmd2

サブコマンドのチェインを使いやすくする

サブコマンドをチェインさせるパターンでは、あるサブコマンドで実行した内容を後続のサブコマンドで更に処理する、というのがよくある。 これを実現するには、まずサブコマンドでオプションや引数を処理する関数を返すようにした上で、それをグループコマンドの resultcallback() デコレータで修飾した関数でまとめて処理する、というやり方があるようだ。 次のサンプルコードでは、サブコマンドが指定されたテキストファイルから行を読みだして大文字にしたり、後ろに感嘆符をつける関数を返している。 そしてそれを @cmd.resultcallback() デコレータで修飾された pipeline() 関数の中で順次適用している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import click


@click.group(chain=True, invoke_without_command=True)
@click.argument('textfile', type=click.File('r'))
def cmd(textfile):
    pass


@cmd.resultcallback()
def pipeline(processors, textfile):
    ite = (line.rstrip('\r\n') for line in textfile)
    for processor in processors:
        ite = processor(ite)
    for line in ite:
        click.echo(line)


@cmd.command()
def shout():
    def processor(ite):
        for line in ite:
            shouted = '{line}!!!'.format(line=line)
            yield shouted
    return processor


@cmd.command()
def upper():
    def processor(ite):
        for line in ite:
            yield line.upper()
    return processor


def main():
    cmd()


if __name__ == '__main__':
    main()

上記を実行すると次のようになる。

$ python pipeline.py greet.txt upper shout
HELLO,!!!
WORLD!!!!

まとめ

今回は Python のサードパーティ製コマンドラインパーサ Click を使ってみた。 Click には、一般的なコマンドラインインターフェースに必要な機能があらかじめ一通り揃っているようだ。 また、その API に関しても argparse などに比べると洗練されていて扱いやすく読みやすいと感じた。