[解決!Python]Clickパッケージを使ってコマンドライン引数を処理するには解決!Python

外部パッケージであるClickは属性ベースでPythonスクリプトに与えられたさまざまな位置引数やオプションを手軽に解析できる。

» 2022年01月18日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「解決!Python」のインデックス

連載目次

clickモジュールの基本

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()


1つのオプションを複数回指定できるようにする

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')


clickモジュールの基本な使い方

 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」となる。これは次のようなルールで決定される(全て小文字となる)。

  1. ハイフン「-」「--」がないものがあれば、それが関数のパラメーター名になる
  2. ハイフン「--」で始まるものがあれば、それから「--」を取り除いたものがパラメーター名になる
  3. ハイフン「-」で始まるものしかなければ、それから「-」を取り除いたものがパラメーター名になる

 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)



 指定したのは全てオプションなので、全て省略できる。全てを省略して実行したのが最初の例だ。次の例では逆に全てのオプションを指定している。

1つのオプションを複数回指定できるようにする

 スクリプト実行時に同一のオプションを複数指定できるようにするには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)程度に制限されている。

オプションをスイッチとして使用

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。