Click 利用ノート¶
概要¶
Python で CLI を書くときに Click はたいへん便利なパッケージだ。次のような機能を搭載している:
Unix/POSIX コマンドライン規約の実装
環境変数からの値の読み込み
カスタム値のプロンプト
入れ子コマンド
ファイル処理
便利補助機能各種
端末寸法取得
ANSI 色使用
キーボード直接入力取得
画面消去
構成ファイルパス検索
テキストエディターなどの起動
インストール・更新・アンインストール¶
複数人で共用するプロジェクトの開発環境に Click をインストールする事例では、そのプロジェクトの定める手順に従え。README や pyproject.toml
を読めば判明する。
自分が所有する作業用仮想環境にインストールするならば、愛用している仮想環境ツールがインストールコマンドを実装している場合にはそれを使え。私ならば Miniconda であるから、例えば次のようにする:
$ 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
にも対応するには、バージョン実引数のすぐ次からキーワード実引数すべての直前までにフラグ名を列挙すればいい:
@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
コマンドヘルプ表示において、当該オプションの既定値をヘルプ画面に出すかどうかを指定する。Click は
False
を既定とするが、一律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()
などに置き換えたり、None
を0.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 検索を試したが、記憶にあるものと一致するものが見つからなかった。