外部パッケージであるClickは属性ベースでPythonスクリプトに与えられたさまざまな位置引数やオプションを手軽に解析できる。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
import click
@click.command()
@click.option('--greet', help='word to greet', default='hello')
@click.argument('to')
def cli(greet, to):
click.echo(f'{greet} {to}')
if __name__ == '__main__':
cli()
import click
@click.group()
def cli():
pass
@cli.command()
def subcmd1():
click.echo('subcmd1')
@click.command()
@click.argument('foo')
def subcmd2(foo):
click.echo(f'subcmd2 with arg {foo}')
cli.add_command(subcmd2)
if __name__ == '__main__':
cli()
import click
@click.command()
@click.option('-l', '--long_long_long', 'long')
@click.option('--foo', default='FOO')
@click.option('--bar', type=float, default=0.0)
@click.option('--baz', nargs=2, type=int, default=(0, 0))
#@click.option('--baz', type=(int, int), default=(0, 0))
def cli(long, foo, bar, baz):
print(f'long: {long}')
print(f'foo: {foo}')
print(f'bar: {bar}')
print(f'baz: {baz}')
if __name__ == '__main__':
cli()
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('-f', '--file', multiple=True)
def cat(file):
for f in file:
with open(f) as f:
click.echo(f.read())
@cli.command()
@click.option('-c', count=True)
def count(c):
click.echo(f'you supecified -c {c} times.')
@cli.command()
@click.argument('files', nargs=-1, type=click.File())
def concat(files):
for f in files:
click.echo(f.read())
if __name__ == '__main__':
cli()
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--silent', is_flag=True)
#@click.option('--silent/--no-silent', default=True)
def chat(silent):
if silent:
click.echo('...')
else:
click.echo('Hello, I am Deep Insider. How are you ? Oh! time to leave')
@cli.command()
@click.option('--upper', 'attr', flag_value='upper', default=True)
@click.option('--lower', 'attr', flag_value='lower')
@click.argument('arg')
def foo(attr, arg):
result = eval(f'"{arg}".{attr}()')
click.echo(result)
if __name__ == '__main__':
cli()
import click
from hashlib import sha256
@click.group()
def cli():
pass
@cli.command()
@click.option('--name', prompt='input your name')
def hello(name):
click.echo(f'hello {name}')
@cli.command()
@click.option('--password', prompt=True, hide_input=True)
def pw(password):
m = sha256()
m.update(password.encode())
print(m.digest())
if __name__ == '__main__':
cli()
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--op', type=click.Choice(['+', '-']))
@click.argument('nums', type=(int, int))
def calc(op, nums):
a, b = nums
if op == '+':
res = a + b
else:
res = a - b
click.echo(f'{a} {op} {b} = {res}')
@cli.command()
@click.option('--to', type=click.IntRange(1, 10), default=1)
def nums(to):
tmp = list(range(0, to+1))
click.echo(tmp)
def validate_if_even(ctx, param, value):
if value % 2:
raise click.BadParameter('num must be even')
return value
@cli.command()
@click.option('--num', type=int, callback=validate_if_even, default=0)
def val(num):
click.echo('even' if num % 2 == 0 else 'odd')
if __name__ == '__main__':
cli()
import click
@click.command()
@click.option('--myenv')
def cli(myenv):
click.echo(f'myenv: {myenv}')
if __name__ == '__main__':
cli(auto_envvar_prefix='DI')
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--myenv')
def env0(myenv):
click.echo(f'myenv: {myenv}')
@cli.command()
@click.option('--myenv1', envvar='MYENV')
def env1(myenv1):
click.echo(myenv1)
@cli.command()
@click.argument('myenv2', envvar='MYENV')
def env2(myenv2):
click.echo(myenv2)
if __name__ == '__main__':
cli(auto_envvar_prefix='DI')
Pythonスクリプトに与えられたコマンドラインを解析するには、前々回に紹介したsys.argv属性や、前回に紹介したargparseモジュールを使用できる。今回紹介するClickパッケージもまた、そうしたパッケージの一つだが、属性ベースでオプションや位置引数を指定したり、コマンドとサブコマンドを簡単に実装できたりする点が前述の2つとは大きく異なる。うまく使えば、少ないコードで高機能なコマンドラインツールを記述できるはずだ。
なお、ClickパッケージはPyPIで配布されている。使用するには「pip3 install click」などを事前に実行してインストールしておく必要がある。
既に述べたが、Clickでは属性ベースでオプションや位置引数を指定する。その基本的な記述方法は次のようになる(位置引数ではオプションほど多くの機能がサポートされていない。これはClickパッケージでは、ファイル名やURLなどは位置引数として処理し、それ以外はオプションとして処理することが推奨されているからだ)。
import click
@click.command() # 修飾した関数はClickのコマンドとなる
@click.option(……) # オプションの指定。オプションはデフォルトで省略可能
@click.argument(……) # 位置引数の指定。位置引数はデフォルトで必須
def cli(……): # スクリプトに与えた値は関数のパラメーターで受け取る
pass # スクリプトに与えられたオプションや位置引数を使って処理を行う
if __name__ == '__main__':
cli()
このように@click.commandデコレーター/@click.optionデコレーター/@click.argumentデコレーターで実際に何らかの処理を行う関数を修飾し、それを呼び出すというのが基本型となる。以下にシンプルな例を示す。
import click
@click.command()
@click.option('--greet', help='word to greet', default='hello')
@click.argument('to')
def hello(greet, to):
click.echo(f'{greet} {to}')
if __name__ == '__main__':
hello()
@click.commandデコレーターはhello関数がClickパッケージの管理下でコマンドとなることを意味している。次の@click.optionデコレーターは「--greet」オプションの指定だ。このとき、「default='hello'」となっているので、省略時にはこのオプションには「hello」が指定されたものと見なされる。helpキーワード引数は、このスクリプトのヘルプを表示したときに使われるテキストだ。@click.argumentデコレーターは位置引数(to)の指定だ。これはオプションではなく、指定が必須となる。
hello関数のパラメーターには、オプションと位置引数の第1引数に指定した「greet」と「to」を並べている点に注意。関数内ではこれらを使って、スクリプトに渡された値を参照できる(上のコードでは「f'{greet} {to}'」でそうしている)。
なお、出力ではprint関数ではなく、click.echo関数を使用しているが、これはClickパッケージが提供する出力用の関数で、こちらを使うことが推奨されている。
幾つか実行例を示す(ファイル名は「clicktest1.py」とする)。
% python3 clicktest1.py world --greet goodbye
goodbye world
% python3 clicktest1.py @IT
hello @IT
1つ目の例は、位置引数(to)を最初に指定して、次にオプションとして「--greet goodbye」を指定している。そのため、デフォルトの「hello」ではなく「goodbye」を使ったあいさつとなる。次の例では、オプション引数は省略して、あいさつする相手だけを指定している。
複雑な処理をするスクリプトでは、「スクリプト サブコマンド --オプション 値 位置引数」のようにして実行することがある。これをサポートするのがサブコマンドと呼ばれる機能だ。このときには、上で見た@click.commandデコレーターではなく、最初に@click.groupデコレーターを使ってグループを作成して、その後、そのグループに所属するサブコマンドを追加していく。
このとき、サブコマンドとなる関数をコマンドとして定義するのに使用するデコレーターは「@click.command」ではなく「@グループ名.command」となるのを忘れないようにしよう。関数名はコマンドラインで指定するサブコマンドの名前となる。
import click
@click.group() # グループの作成
def cli():
pass
@cli.command() # サブコマンドを追加(@click.commandではない点に注意)
@click.option(……)
@click.argument(……)
def subcmd():
pass
あるいは、@click.commandデコレーターを使って、一度コマンドを記述した後に、グループのadd_commandメソッドにそのコマンドを追加してもよい。
import click
@click.group() # グループの作成
def cli():
pass
@click.command() # コマンドを追加
@click.option(……)
@click.argument(……)
def subcmd():
pass
cli.add_command(subcmd) # subcmd関数(コマンド)をグループに追加
実際のコード例を以下に示す。
import click
@click.group()
def cli():
pass
@cli.command()
def subcmd1():
click.echo('subcmd1')
@click.command()
@click.argument('foo')
def subcmd2(foo):
click.echo(f'subcmd2 with arg {foo}')
cli.add_command(subcmd2)
if __name__ == '__main__':
cli()
ここでは2つのサブコマンド(subcmd1、subcmd2)を追加している。subcmd1は@cli.commandデコレーターを使ってサブコマンドとしている。また、このサブコマンドにはオプションの指定も位置引数の指定もないので、このコマンドは「スクリプト名 subcmd1」のようにして実行する。
subcmd2は@click.commandデコレーターを使ってコマンドを定義した後に、「cli.add_command(subcmd2)」としてグループに追加している。また、こちらでは位置引数(foo)が指定されているので、このコマンドは「スクリプト名 subcmd2 値」のようにして呼び出す。
実行例を以下に幾つか示す(ファイル名は「clicktest2.py」とする)。
% python3 clicktest2.py subcmd1
subcmd1
% python3 clicktest2.py subcmd2 hogehoge
subcmd2 with arg hogehoge
最初の例は「スクリプト名 subcmd1」のようにして、subcmd1サブコマンドを呼び出している。次の例は「スクリプト名 subcmd2 hogehoge」のようにしてsubcmd2サブコマンドを呼び出すものだ。
ここまでがClickパッケージの基本的な使い方となる。以下では、Clickパッケージが提供するさまざまなオプション(の一部)を見ていく。詳細についてはClickパッケージのドキュメントを参照されたい。
オプションでは指定する値の型や、デフォルト値、関数から参照する際に使用する名前などを指定できる。以下に例を示す。
import click
@click.command()
@click.option('-l', '--long_long_long', 'long')
@click.option('--foo', default='FOO')
@click.option('--bar', type=float, default=0.0)
@click.option('--baz', nargs=2, type=int, default=(0, 0))
#@click.option('--baz', type=(int, int), default=(0, 0))
def cli(long, foo, bar, baz):
print(f'long: {long}')
print(f'foo: {foo}')
print(f'bar: {bar}')
print(f'baz: {baz}')
if __name__ == '__main__':
cli()
最初のオプションでは「'-l', '--long_long_long', 'long'」のような指定が行われている。このときには関数で参照する際に使用する名前は「long」となる。これは次のようなルールで決定される(全て小文字となる)。
2つ目のオプションはデフォルト値を指定する例だ。このオプションを省略したときには関数のfooパラメーターには「FOO」が渡される。「default=……」としてデフォルト値を指定しなかったときにはデフォルト値はNoneとなる。
3つ目のオプションはオプションに指定する値の型を指定する例だ。この例では「type=float」となっているので、ユーザーがスクリプトに与えた値は浮動小数点数値としてスクリプトに渡される。もちろん、浮動小数点数値に変換できなければ例外となる。型を指定しなければ、文字列として関数に渡される。
4つ目のオプションは、そのオプションが受け取る値の数を指定するものだ。この例では「nargs=2」「type=int」となっているので、整数値を2つ受け取る。nargsに2以上を指定すると、それらはタプルとして渡される。そのため、デフォルト値も「default=(0, 0)」のようにしている。なお、このオプションはその下にコメントアウトされているように「type=(int, int)」としてもよい。
実行例を以下に示す(ファイル名は「opttest1.py」とする)。
% python3 opttest1.py
long: None
foo: foo
bar: 0.0
baz: (0, 0)
% python3 opttest1.py -l @IT --foo BAR --bar 1.2 --baz 10 20
long: @IT
foo: BAR
bar: 1.2
baz: (10, 20)
指定したのは全てオプションなので、全て省略できる。全てを省略して実行したのが最初の例だ。次の例では逆に全てのオプションを指定している。
スクリプト実行時に同一のオプションを複数指定できるようにするにはmultiple引数かcount引数をTrueにする。前者はそのオプションに渡された値がタプルに格納されて関数に渡される(ということは、デフォルト値を指定する場合はそれもタプルまたはリストとして指定する必要がある)。後者はそのオプションが何回指定されたか、その回数を関数に渡してくれる。
以下に例を示す。ここでは両者のオプションの使われ方を見るために、それらをサブコマンドとして定義している。ファイル名は「opttest2.py」とする。
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('-f', '--file', multiple=True)
def cat(file):
for f in file:
with open(f) as f:
click.echo(f.read())
@cli.command()
@click.option('-c', count=True)
def count(c):
click.echo(f'you supecified -c {c} times.')
@cli.command()
@click.argument('files', nargs=-1, type=click.File())
def concat(files):
for f in files:
click.echo(f.read())
if __name__ == '__main__':
cli()
最初のサブコマンドは「opttest2.py cat -f ファイル名 --file ファイル名」のようにして複数のファイルを指定すると、その内容をまとめて表示するものだ。次のサブコマンドは「opttest2.py count -c -c」のようにして「-c」が指定された回数を調べる。
なお、最初のサブコマンドではわざわざ「-f」「-file」を何度も書くのが面倒だ。そういうときには、オプションではなく位置引数を使って、最後の例のように書くとよい。この例では「nargs=-1」を指定しているが、これは任意の個数の値を受け取ることを意味する。また、引数の型としては「click.File()」を指定している。こうすると、関数にはオープン済みのファイルオブジェクトが渡される。そのため、最初のオプションとは異なり、open関数でファイルをオープンする手順が省略されている。
以下に実行例を示す。
% python3 opttest2.py cat -f a.txt --file b.txt
a.txt
a.txt
b.txt
b.txt
% python3 opttest2.py concat a.txt b.txt
a.txt
a.txt
b.txt
b.txt
% python3 opttest2.py count -c -c -c
you supecified -c 3 times.
なお、ファイルのパスを扱うのであれば「type=click.Path()」も使える。これについてはClickパッケージのドキュメント「File Path Arguments」を参照のこと。ちなみに@click.argumentデコレーターで指定できるのは、引数の数(nargs)、値の種類(type)、最後に紹介する環境変数からの読み出し(envvar)程度に制限されている。
オプションはプログラムの振る舞いを変更するスイッチのように使われることもある。これをClickパッケージで行うには、is_flagキーワード引数にTrueを指定する方法、複数のオプションで1つのパラメーターを共有して、どちらのオプションが指定されたかでその値を振り分ける方法がある。
前者はさらに、あるオプションが指定されたかどうかでフラグの真偽を反転する方法と、2つのオプションをスラッシュで区切る方法がある。以下のコード例では両方の方法を記述して後者についてはコメントアウトしてあるので、自分で動作を確認してほしい(コメントアウトされたコードでは「is_flag=True」がないが、これは「--オプション/--オプション」としてオプションが指定された場合は暗黙的にこのフラグがTrueとなるからだ)。
後者の方法は2つのオプションで同じパラメーターに値を代入するようにする。以下の例では、「--upper」オプションが指定されたらattrパラメーターに「'upper'」を、「--lower」オプションが指定されたら「'lower'」を指定するようになっている(デフォルトは「--upper」となっている)。そして、位置引数に渡された文字列をupperメソッドかlowerメソッドで大文字化/小文字化する。
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--silent', is_flag=True, default=False)
#@click.option('--silent/--verbose', 'silent', default=True)
def chat(silent):
if silent:
click.echo('...')
else:
click.echo('Hello, I am Deep Insider. How are you ? Oh! time to leave')
@cli.command()
@click.option('--upper', 'attr', flag_value='upper', default=True)
@click.option('--lower', 'attr', flag_value='lower')
@click.argument('arg')
def foo(attr, arg):
result = eval(f'"{arg}".{attr}()')
click.echo(result)
if __name__ == '__main__':
cli()
実行例を以下に示す(ファイル名は「opttest3.py」とする)。
% python3 opttest3.py chat
Hello, I am Deep Insider. How are you ? Oh! time to leave
% python3 opttest3.py chat --silent
...
% python3 opttest3.py foo abc
ABC
% python3 opttest3.py foo --lower ABC
abc
オプションの指定がなかったときにはプロンプトを表示して入力を促したり、パスワード入力用のプロンプトを表示して、入力時にはエコーバックをしないようにしたりといったことも可能だ。
前者はpromptキーワード引数にプロンプトを指定する。後者はpromptキーワード引数にTrueを指定して、hide_inputキーワード引数にもTrueを指定する。以下に例を示す。
import click
from hashlib import sha256
@click.group()
def cli():
pass
@cli.command()
@click.option('--name', prompt='input your name')
def hello(name):
click.echo(f'hello {name}')
@cli.command()
@click.option('--password', prompt=True, hide_input=True)
def pw(password):
m = sha256()
m.update(password.encode())
print(m.digest())
if __name__ == '__main__':
cli()
実行例を以下に示す。
% python3 opttest4.py hello
input your name: world
hello world
% python3 opttest4.py pw
Password:
b'\x97\xdf5\x88\xb5\xa3\xf2K\xab……\x14\xb9\xd0ia\xbf\xc1p}\x9d'
オプションに渡される値を特定のものに制限したり、渡された値を検証したりもできる。制限するには@click.optionデコレーターのtypeキーワード引数にclick.Choiceインスタンスを指定したり、click.IntRangeインスタンスを指定したりする。検証を行うには、コールバック関数を定義して、それが呼び出されるようにする。
以下に例を示す。
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--op', type=click.Choice(['+', '-']))
@click.argument('nums', type=(int, int))
def calc(op, nums):
a, b = nums
if op == '+':
res = a + b
else:
res = a - b
click.echo(f'{a} {op} {b} = {res}')
@cli.command()
@click.option('--to', type=click.IntRange(1, 10), default=1)
def nums(to):
tmp = list(range(0, to+1))
click.echo(tmp)
def validate_if_even(ctx, param, value):
if value % 2:
raise click.BadParameter('num must be even')
return value
@cli.command()
@click.option('--num', type=int, callback=validate_if_even, default=0)
def val(num):
click.echo('even' if num % 2 == 0 else 'odd')
if __name__ == '__main__':
cli()
最初の例では「type=click.Choice(['+', '-'])」のようにして、「--op」オプションに指定できるのが「+」か「-」だけとしている。次の例では1〜10の範囲の整数だけしか指定できないようにしている。なお、上の例では使っていないが、このときに「clamp=True」を指定すると、範囲外の値が指定されたときには上限または下限の値が指定されたものと見なされるようにできる。IntRangeに指定した上限値は指定できることには注意しよう。
最後の例では、validate_is_even関数を定義して、それを@click.optionデコレーターのcallbackキーワード引数に与えている。これにより、値が入力された後にvalidate_is_even関数が呼び出されて、そのコードが実行される。valueパラメーターにその値が渡されるので、ここでは奇数であれば例外を発生させて、偶数であればその値を返すようにしてある。
実行例を以下に示す(ファイル名は「opttest5.py」とする)。
% python3 opttest5.py calc --op + 10 20
10 + 20 = 30
% python3 opttest5.py nums --to 10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
% python3 opttest5.py val --num 10
even
% python3 opttest5.py val --num 11
Usage: opttest5.py val [OPTIONS]
Try 'opttest5.py val --help' for help.
Error: Invalid value for '--num': num must be even
numsサブコマンドでは上限値を「IntRange(1, 10)」と指定したが、Pythonの一般的な感覚とは異なり、10を指定できている点に注意しよう。もう1つ、最後の例では奇数を--numオプションに指定しているのでエラーが発生した点にも注目されたい。
最後に環境変数からの値の取得についても簡単に見ておく。スクリプト実行時には、オプションや位置引数を用いて値を入力してもらってもよいが、環境変数にあらかじめ設定してある値を読み取れると便利なことがよくある。Clickパッケージではデフォルトでこの機能がサポートされている。
以下はそうしたコードの例だ。
import click
@click.command()
@click.option('--myenv')
def cli(myenv):
click.echo(f'myenv: {myenv}')
if __name__ == '__main__':
cli(auto_envvar_prefix='DI')
これはコマンド(サブコマンドではなく)を例としたもので、この場合は最後のcli関数呼び出しで「auto_envvar_prefix='DI'」としている点に注目されたい。コマンドの場合、ここで指定したプリフィックスにアンダースコア「_」とオプションで指定した「myenv」を大文字化した「MYENV」をつなげた「DI_MYENV」という名前の環境変数に格納されている値が自動的に読み出される。
実行例を以下に示す(ファイル名は「cmdenvtest.py」とする)。
% export DI_MYENV='Deep Insider at @IT'
% python3 cmdenvtest.py
myenv: Deep Insider at @IT
スクリプトを実行する前に環境変数DI_MYENVに設定した値が読み出されていることが確認できる。
サブコマンドの場合は、上で見たプリフィックスとサブコマンド名、それからオプションの名前をアンダースコアでつなげた名前の環境変数が自動的に読み出される。あるいは@click.optionデコレーターのenvvarキーワード引数で指定した名前の環境変数を読み取ることも可能だ。
以下に例を示す。
import click
@click.group()
def cli():
pass
@cli.command()
@click.option('--myenv')
def env0(myenv):
click.echo(f'myenv: {myenv}')
@cli.command()
@click.option('--myenv1', envvar='MYENV')
def env1(myenv1):
click.echo(myenv1)
@cli.command()
@click.argument('myenv2', envvar='MYENV')
def env2(myenv2):
click.echo(myenv2)
if __name__ == '__main__':
cli(auto_envvar_prefix='DI')
最初の例ではenv0サブコマンドで環境変数を読み出そうとしているが、ここではenvvarキーワード引数の指定がない。そのため、プリフィックスである「DI」とサブコマンドの名前を大文字化した「ENV0」とオプションの名前を大文字化した「MYENV」を連結した「DI_ENV0_MYENV」という環境変数から値を読み取る。
次の例はenvvarキーワード引数にMYENVを指定しているので、環境変数MYENVから値が読み出される。最後の例は位置引数の場合だ。これも環境変数MYENVから値が読み出される。
実行例を以下に示す(ファイル名は「envtest.py」とする)。
% export DI_ENV0_MYENV='value for env0 subcmd'
% export MYENV='value for env1 and env2 subcmd'
% python3 envtest.py env0
myenv: value for env0 subcmd
% python3 envtest.py env1
value for env1 and env2 subcmd
% python3 envtest.py env2
value for env1 and env2 subcmd
なお、envvarキーワード引数で指定した環境変数が存在しない場合にはエラーとなる。一方、自動的に読み出す対象の環境変数が存在しない場合はその値はNoneとなる。
Copyright© Digital Advantage Corp. All Rights Reserved.