Click 利用ノート

概要

Python で CLI を書くときに Click はたいへん便利なパッケージだ。次のような機能を搭載している:

  • Unix/POSIX コマンドライン規約の実装

  • 環境変数からの値の読み込み

  • カスタム値のプロンプト

  • 入れ子コマンド

  • ファイル処理

  • 便利補助機能各種

    • 端末寸法取得

    • ANSI 色使用

    • キーボード直接入力取得

    • 画面消去

    • 構成ファイルパス検索

    • テキストエディターなどの起動

インストール・更新・アンインストール

複数人で共用するプロジェクトの開発環境に Click をインストールする事例では、そのプロジェクトの定める手順に従え。README や pyproject.toml を読めば判明する。

自分が所有する作業用仮想環境にインストールするならば、愛用している仮想環境ツールがインストールコマンドを実装している場合にはそれを使え。私ならば Miniconda であるから、例えば次のようにする:

現在の conda 仮想環境に Click をインストールする
$ conda install -c conda-forge click

インストール手順の説明は以上だ。Click の更新、アンインストールの手順は、対応する条件におけるインストール手順に合致する手順を選べ。例えば conda を使っているのならば conda uninstall click を走らせる。

See also

Miniconda 利用ノート

conda を使ってインストールする場合、更新やバージョン確認にもそれを用いる。

使用方法・コツ

単一コマンドしかないような単純なスクリプトを作成する場合ですらよく使う手筋を記す。

画面への出力には関数 click.echo を使え

公式文書によれば、関数 click.echo を Python 標準 print() の代わりとしてなるべく使えとある。さまざまなデータ、ファイル、環境に対してより良く働く。

個人的に好きな性質を挙げる:

  • 色や単純な書式付きでテキストを出力可能。例えば赤い太字とか。

  • 出力が対話型端末でなさそうな場合、ANSI 色とスタイルコードを削る。

  • 出力をつねにフラッシュする。

Todo

Python 標準 logging との棲み分けは?

Python 関数をコマンドに仕立てる

Click を用いるもっとも単純なスクリプトは次のようなコードだ:

import click を含む最小スクリプト
import click

@click.command()
def main():
    """Output a text."""
    click.echo("Hello world.")

if __name__ == "__main__":
    main()

このスクリプトの名前を helloworld.py とすると、これだけで次のコマンドラインが有効だ:

  • 当然ながら helloworld.py のみ。

  • helloworld.py --help: オプション --help も自動的に組み込まれる。

このオプションを付けてスクリプトを走らせると、ヘルプを表示して終了する。そのときの本文はコマンド関数の docstring から構成される。Click はこのテキストを端末画面の幅に合わせて折り返し表示する。エディターに入力したとおりに画面に出力させるには制御文字 \b を用いる。解除は \f だ。

オプション --help を調整する

既存のオプション説明文と整合させるために @click.help_option などを明示的に用いて --help 自身のヘルプ文言を独自化することが可能だ。

@click.help_option を使って説明文を自分で決める
import click

@command()
@click.help_option(help="show this message and exit")
def main(): ...

if __name__ == "__main__":
    main()

オプション --version を実装する

Click が用意している @click.version_option を再利用するのが手っ取り早い。コマンド定義関数からアプリケーションまたはパッケージのバージョン文字列が参照可能である場合には次のようにするのが自然だ:

@click.version_option を使って --version を実装する
import click

__version__ = "1.0.0"

@command()
@click.version_option(__version__, help="show the version and exit")
def main(): ...

if __name__ == "__main__":
    main()

このスクリプトの名前を myapp.py とすると、これだけでコマンドライン myapp.py --version が有効となる。

フラグ名を --version だけではなく -V にも対応するには、バージョン実引数のすぐ次からキーワード実引数すべての直前までにフラグ名を列挙すればいい:

“-V” と “–version” の両方をバージョンフラグとする
@command()
@click.version_option(__version__, "-V", "--version")
def main(): ...

より詳細なバージョン出力を備えたい場合には version_option ではなく、汎用の option を用いる。さらにコールバックで実装する:

@click.option を使って --version を自前で実装する
import sys
import click

def print_version(
    ctx: click.Context,
    param: click.Parameter,
    value: bool,
) -> None:
    """Display version information and exit."""

    if not value or ctx.resilient_parsing:
        return

    click.echo(f"myapp.py: {__version__}")
    click.echo(f"Click: {click.__version__}")
    click.echo(f"Python: {sys.version}")
    ctx.exit()


@click.command()
@click.option(
    "-V",
    "--version",
    is_flag=True,
    callback=print_version,
    expose_value=False,
    is_eager=True,
    help="display version information and exit",
)
def main(): ...

コマンドライン引数をファイルパスとする

ファイルパスを引数にとるスクリプトを作成する機会は頻繁にある。コマンドライン引数として複数のパス文字列を取るコマンドを作る場合には、次のようにするのがよい。

type=click.Path の適用例
import pathlib
import click

@click.command()
@click.argument(
    "file",
    nargs=-1,
    type=click.Path(
        exists=True,
        path_type=pathlib.Path,
    ),
)
def main(file): ...

Python コードではコマンド関数 main の最初の仮引数名が @click.argument の最初の実引数値と同じになる。

  • myapp.py file1 file2 のようなコマンドが許される。

  • nargs=-1 のおかげでパスを全く指定しないコマンドも許される。この手のインターフェイスはそう設計するのが鉄則だ。

  • 急所は type=click.Path(...) だ。これは引数 file がファイルシステムの有効なパス文字列であることを保証する。コンストラクターに渡す値により、そのパス文字列の条件を柔軟に指定することが可能だ。例えば、

    • exists=True により、存在するファイルパスしか指定を許さない。

    • path_type=pathlib.Path により、関数 main の引数としての file の型を pathlib.Path に変換させる。後続のパス操作に便利であるがゆえ、このパス型指定を与えたい。

@click.option 系デコレーターでよく使うキーワード引数

@click.option 系デコレーターでよく使うキーワード引数はクラス Option のコンストラクターが取る引数とだいたい一致する。よく使用するものを下に載せる:

help

ヘルプ文字列。自作オプションに対しては必ず指定しろ。

type

オプション値の型。これを適切に指定しておくと、Click がコマンドラインからの入力値を検証してから、値を所望の型に変換するか、エラーで終了する。Python 組み込み型を渡す場合もあるが、パスや日付など、土台は文字列だが特別な書式をとる値に対して機能する専用型も Click は備えている。後ほど個別に記すが、例を挙げる:

  • click.Choice

  • click.DateTime

  • click.File

  • click.Path

is_flag

オプションをフラグとして機能させたい場合には値を True に明示的に指示しろ。指定しない場合には Click がオプション型を自動的に判断する。

show_default

コマンドヘルプ表示において、当該オプションの既定値をヘルプ画面に出すかどうかを指定する。ClickFalse を既定とするが、一律 True でいいと思う。

show_envvar

当該オプションが環境変数に対応している場合、コマンドヘルプ表示にその変数名を示すかどうかを指定する。一律 True でいいと思う。

構成ファイル実装

Git でいうところのファイル .gitconfig のような機能を実現する手順を記す。

まず、コマンドラインオプション -c FILE または --config FILE で構成ファイルを指定するインターフェイスを定義する:

オプション --config 搭載例
import pathlib
import click

@click.command()
@click.option(
    "-c",
    "--config",
    type=click.Path(exists=True, path_type=pathlib.Path),
    default=None,
    metavar="PATH",
    callback=configure,
    is_eager=True,
    expose_value=False,
    help="path to config file",
)
def main(): ...

キーワード引数を指定する狙いは次のとおり:

  • type=click.Path(...) の行の目的は先述のとおり、既存のファイルパスを与えられることを保証したい。

  • is_eager=True であるオプション値は、そうでない値のものより先に処理される。構成ファイル処理を急いているのだ。これを利用して構成ファイルを先に読み込むのが目的だ。

  • expose_value=False を指定して、関数 main の引数リストに対応する引数を与えなくて済むようにする。構成ファイル処理を main で行うわけではないのだ。

  • callback=configure を指定して、構成ファイルを読み込む関数を呼び出すように指示する。ここでは関数 configure を呼び出させる。

別途コールバック関数 configure を実装する。次の例のコードは構成ファイルの書式を YAML であるとしている。

構成ファイル読み込み例
import yaml

def configure(ctx, param, value):
    if value:
        assert isinstance(value, pathlib.Path)
        with open(value, mode="r") as fin:
            ctx.default_map = yaml.safe_load(fin)
    return value

辞書 ctx.default_map とはコマンドラインオプションの既定値を含むデータであり、 YAML ファイル内容の値を使ってそれを初期化するという理解でいい。

関数 click.get_app_dir

構成ファイル読み込み処理にも関連するのだが、その既定パスを決定するのに関数 get_app_dir が有用だ。アプリケーションやパッケージの名前を指定すると、その構成ディレクトリーパス文字列を得られる。

既定動作では OS に最適のパスを返す。例えば WSL を含む Linux では:

click.get_app_dir 使用例
>>> import click
>>> APP_NAME = "myapp"
>>> click.get_app_dir(APP_NAME)
'/home/username/.config/myapp'
>>> click.get_app_dir(APP_NAME, force_posix=True)
'/home/username/.myapp'

これを先述した構成ファイル読み込みコードに組み込むのが良い。オプション -c, --config が与えられていない場合には、既定の構成ファイルパスとしてこの値を用いるとらしくなる。

気の利いた引数型

デコレーター @click.option キーワード引数 type に指定可能である専用型のうち、特に便利なものをいくつか記す。

入力値の集合がある場合には click.Choice を使え

使用例を次に示す。キーワード引数 case_sensitive の既定値が True であるのが不便であると考えられる場合には False に変えろ:

click.Choice 使用例
@click.command()
@click.option(
    "-f",
    "--format",
    type=click.Choice(
        ("json", "jsonline", "xml", "csv"),
        case_sensitive=False,
    ),
    default="json",
)
def main(format): ...

日付または時刻に click.DateTime を使え

時刻まで欲しい場合には click.DateTime コンストラクターを引数指定なしで呼び出せば十分だ。

時刻不要の日付を扱う場合はコード側での対応が生じる場合がある。次のように指定する場合でも click.DateTime オブジェクトは時刻情報を有する:

click.DateTime 使用例
@click.command()
@click.option(
    "--since",
    type=click.DateTime(("%Y-%m-%d",)),
    metavar="DATE",
)
def main(since): ...

日付部分だけを得るには、呼び出し側で `` 00:00:00`` 部分をトリムしかない。

ファイル内容には click.File を使え

公式リポジトリーにある次のファイルが使用例だ:

Python コード中で標準入力と普通のファイルを区別せずに扱うのがこの型の目的だ。関連して、関数 click.open_file は同様の思想で設計されている。

ファイルパスには click.Path を使え

先述した コマンドライン引数をファイルパスとする のとおり。

実行中に入力を受け付けてもよい場合は prompt=True を使え

スクリプト実行中に端末からのキーボード入力による値を受け付けたい。この場合にはキーワード引数 prompt=True を指定しろ。使用例:

prompt=True 使用例
@click.command()
@click.option(
    "-u",
    "--user",
    prompt=True,
    hide_input=False,
    metavar="NAME",
)
def main(user): ...

コマンドラインから -u または --user をその実引数と共に指定して実行するとプロンプトが出ることなくプログラムが進行する。どちらも指定せずに実行すると User: のプロンプトで標準入力が開き、キーボード入力を待機する。

パスワード入力には @click.password_option を使え

上述の要領でパスワードオプションを定義しても良いが、ありがちなオプションであるので Click がすでに用意している:

@click.password_option 使用例
import click

@click.command()
@click.password_option(metavar="PASSWORD")
def main(password): ...

この例ではコマンドラインオプションは --password となり、コマンド関数の引数名は password となる。コマンドラインでパスワードを指定することが可能であるうえ、未指定実行時にはプロンプト入力方式になる。

キーボード入力時にタイプしたキーは端末にエコーされない。

プロンプトは確認用と合わせて二度出現する。

その他

  • @click.pass_context について述べたい。

  • 構成ファイルに関連して環境変数からオプション値を読み込む方法を扱いたい。

資料集

Click Documentation

公式文書。

Click and Python: Build Extensible and Composable CLI Apps

チュートリアル。Real Python の記事。Click を用いたプログラムを配布するためのプロジェクト構成ファイル pyproject.toml の書き方を示しているのはありがたい。

Working with Python Click Package

紹介とささやかなチュートリアルからなる記事。Python パッケージを説明する前に、開発の全ては仮想環境で行うと断れば、残りの記述が簡潔になりがちになることを習った。

Python click or how to write professional CLI applications

詳しめの紹介記事。自作 JSON 解析スクリプトのリファクタリングを通じて上手く説明している。このコードをそのまま受け入れて読むのではなく、例えば print()click.echo() などに置き換えたり、None0.0 に変えるなど、改良点や修正点を探しつつ読め。

標準の argparse にあって Click にはない機能を挙げている記事は初めて見た。

Advanced CLI structures with Python and Click

少し発展的な機能を紹介する記事。コマンド集約や独自入力検証など。ブログ内には Click 関連記事が他にもある。

command line interface - Python Click - Supply arguments and options from a configuration file

構成ファイルの実装方法が複数示されている。

Todo

バージョン実装の参考になった資料があったのだが、URL を忘れた。あらためて Google 検索を試したが、記憶にあるものと一致するものが見つからなかった。