rcmdnk's blog

20190128_pythonfire_200_200

最近(でもないですが) 色々なツールでcmd <subcommand>と言った形でサブコマンドを引数に取るような ものが増えてきました。

Pythonで素直に実装しようとするとArgumentParserとかを使って頑張る事になりますが、 Google製のPython Fireを使うと引数の取り回しを考えることなく そのようなツールを簡単に実装することができます。

Sponsored Links

Python Fire

Overview - Python Fire

Python Fireはコマンドラインツール(CLI)をクラスや関数から 簡単に作成するライブラリです。

使い方としては次の様にクラスなどを作ってそれを 実行時にfire.Fireに喰わせるだけ。

クラスを使う

クラスを使う場合はこんな感じ。

calc_class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python

import fire


class Calculator(object):
    """A simple calculator class."""
    def __init__(self, base=10):
        self.base = base

    def add(self, num):
        """Addition function."""
        return self.base + num

    def sub(self, num):
        """Subtraction function."""
        return self.base - num


if __name__ == '__main__':
    fire.Fire(Calculator)

これでクラス内にあるaddsubという関数名がそのまま サブコマンドとして使える様になります。

$ chmod 755 calc_class
$ ./calc_class
Type:        Calculator
String form: <__main__.Calculator object at 0x10b7084d0>
File:        ~/calc_class
Docstring:   A simple calculator class.

Usage:       calc_class -
             calc_class - add
             calc_class - base
             calc_class - sub
$ ./calc_class add
Fire trace:
1. Initial component
2. Instantiated class "Calculator" (./calc_class:6)
3. Accessed property "add" (./calc_class:11)
4. ('The function received no value for the required argument:', 'num')

Type:        instancemethod
String form: <bound method Calculator.add of <__main__.Calculator object at 0x101fb74d0>>
File:        ~/calc_class
Line:        11
Docstring:   Addition function.

Usage:       calc_class add NUM
             calc_class add --num NUM
$ ./calc_class add 1
11
$ ./calc_class add --num 1
11
$ ./calc_class sub 1
9
$ ./calc_class add --base 5 1
6
$ ./calc_class sub --base 5 1
4

みたいな感じになります。

関数がサブコマンドになりますが、その引数はサブコマンドのあとに直接書く形で渡します。 --<引数名>の様な形で渡すことも出来ます。

また、クラスをPython Fireに渡した場合、 --<コンストラクタの引数名>とすることでコンストラクタの引数を渡すことも出来ます。

ちなみに最初のところでUsagebaseも入ってしまっていますが Python Fireでは基本的に渡したObjectが持っているすべての通常Object(__init__の様な__で始まる特殊オブジェクト以外) がサブコマンドとして登録されます。

変数の場合には単にその値が返されます。

$ ./calc base
10

関数を使う

同じ様なことを関数だけでも作ることが出来て

calc_func
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python

import fire


def add(num1, num2):
    """Addition function."""
    return num1 + num2


def sub(num1, num2):
    """Subtraction function."""
    return num1 - num2


if __name__ == '__main__':
    fire.Fire()

こんな感じのスクリプトを作って

$ ./calc_func
fire: <module 'fire' from '/usr/local/lib/python2.7/site-packages/fire/__init__.pyc'>
add:  <function add at 0x1057e9668>
sub:  <function sub at 0x105ccd7d0>
$ ./calc_func add
Fire trace:
1. Initial component
2. Accessed property "add"
3. ('The function received no value for the required argument:', 'num1')

Type:        function
String form: <function add at 0x10d8f6668>
File:        ~/calc_func
Line:        6
Docstring:   Addition function.

Usage:       calc_func add NUM1 NUM2
             calc_func add --num1 NUM1 --num2 NUM2
$ ./calc_func add 1 2
3
$ ./calc_func sub 1 2
-1

こんな感じで使えます。

fire.Fireを引数無しで呼ぶとそのファイルにあるオブジェクトを一通り組み込む様な感じになります。

関数をFireに直接与える

この場合に関数を与えると

calc_func2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python

import fire


def add(num1, num2):
    """Addition function."""
    return num1 + num2


def sub(num1, num2):
    """Subtraction function."""
    return num1 - num2


if __name__ == '__main__':
    fire.Fire(add)
$ ./calc_func2
Fire trace:
1. Initial component
2. ('The function received no value for the required argument:', 'num1')

Type:        function
String form: <function add at 0x1026e2668>
File:        ~/calc_func2
Line:        6
Docstring:   Addition function.

Usage:       calc_func 2NUM1 NUM2
             calc_func 2--num1 NUM1 --num2 NUM2
$ ./calc_func2 1 2
3

の様にaddだけが直接組み込まれます。

クラスのあるファイルでFireに引数を与えない場合

逆に最初のクラスの場合だと

$ ./calc_class2
fire:       <module 'fire' from '/usr/local/lib/python2.7/site-packages/fire/__init__.pyc'>
Calculator: <class '__main__.Calculator'>
$ ./calc_class2 Calculator
Type:        Calculator
String form: <__main__.Calculator object at 0x10277c4d0>
File:        ~/calc_class2
Docstring:   A simple calculator class.

Usage:       calc_class2 Calculator -
             calc_class2 Calculator - add
             calc_class2 Calculator - base
             calc_class2 Calculator - hoge
             calc_class2 Calculator - sub
$ ./calc_class2 Calculator add 1
11

こんな感じでファイルにあるCalculatorが最初のサブコマンドとして認知され、 addなどの関数がそのサブサブコマンドとして使える様になります。

サブコマンドのグループ化

こういったオブジェクト内のオブジェクトがさらにまたサブコマンドになる、といった事を利用すると サブコマンド付きのサブコマンド、といったものが簡単に作れる様になります。

calc_group
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python

import fire


class Addition(object):
    def calc(self, num1, num2):
        print(num1 + num2)

    def name(self):
        print("Addition")


class Subtraction(object):
    def calc(self, num1, num2):
        print(num1 - num2)

    def name(self):
        print("Subtraction")


class Calculator(object):
    """A calculator group example."""
    def __init__(self):
        self.add = Addition()
        self.sub = Subtraction()


if __name__ == '__main__':
    fire.Fire(Calculator)
$ ./calc_group
Type:        Calculator
String form: <__main__.Calculator object at 0x10cf4a4d0>
File:        ~/calc_group
Docstring:   A calculator group example.

Usage:       calc_group
             calc_group add
             calc_group sub
$ ./calc_group add
Type:        Addition
String form: <__main__.Addition object at 0x10ff25510>
File:        ~/calc_group

Usage:       calc_group add
             calc_group add calc
             calc_group add name
$ ./calc_group add calc 1 2
3
$ ./calc_group sub calc 1 2
-1
$ ./calc_group sub name
Subtraction
$

さらにサブコマンドをネストしたければ上のAdditionでコンストラクタを作って そこに新たなサブコマンド用のクラスをオブジェクトとして定義すれば良いだけです。

このあたりまで他のライブラリや自作で行おうとするとかなり面倒なことになるかと思いますが Python Fireだとこんな感じで簡単に出来ます。

helpについて

Python Fireでは上にも見た様にスクリプトだけ実行したり、 引数が必要なサブコマンドを引数無しで実行したりすると 自動である程度のヘルプが表示されます。

また、関数などの説明をdocstring(先頭にあるコメント)で定義しておくことで それを表示してくれます。

関数の説明も

$ ./calc_class add -- help
Fire trace:
1. Initial component
2. Instantiated class "Calculator" (./calc_class:6)
3. Accessed property "add" (./calc_class:12)
4. ('The function received no value for the required argument:', 'num')

Type:        instancemethod
String form: <bound method Calculator.add of <__main__.Calculator object at 0x10ba714d0>>
File:        ~/calc_class
Line:        12
Docstring:   Addition function.

Usage:       calc_class add NUM
             calc_class add --num NUM

この様に--で区切ったあとにhelpを渡してあげることで関数のヘルプを呼び出すことが出来、 ここでも関数のdocstringが表示されていることが分かります。

なのでdocstringをきちんと書いておくと何もせずにそれなりのヘルプを作ることも可能です。

ただ、-- helpと呼ばないといけない、というのは知ってないと出来ないのでちょっと不親切です。

また、最初のクラスの例でbaseがサブコマンドとして表示されてしまうのも

ただ、最初のクラスの例だとコンストラクタで作るbaseがサブコマンドとして 表示されたりするのもちょっと嫌です。

なのでできればhelpという関数を自分で 以下のようにすると良いかな、と思っています。

calc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python

import sys
import fire


class Calculator(object):
    """A simple calculator class."""
    def __init__(self, base=10):
        self.base = base
        self.hoge = 'hoge'

    def add(self, num):
        """Addition function."""
        return self.base + num

    def sub(self, num):
        """Subtraction function."""
        return self.base - num

    @staticmethod
    def help():
        print("""Usage: calc [--base <num_base>] <command> ...

Commands:
    add <num>     Return <num_base> + <num>
    sub <num>     Return <num_base> - <num>
    help          Show this help

Option:
    --base <num>  Set <num_base>""")


if __name__ == '__main__':
    if len(sys.argv) == 1:
        Calculator.help()
    else:
        fire.Fire(Calculator)

引数がないときは直接helpを呼ぶ様にしています。 まあそのあたりは好みですが。

似たようなライブラリ

似たようなコマンドラインツール用のライブラリでclickというものもあります。

Click The Pallets Projects

こちらはまさにコマンドラインツールを作るのにArgparseが使いづらいからそれを なんとかした、という感じのもので デコレーターを使って引数を定義して行く感じになっています。

ヘルプなどの仕上がりも綺麗で、 サブサブコマンド的なネストも出来ます。

他にもいろいろと機能があるのでそのあたりで使いたい機能があるのであれば Clickを使ってみるのも良いかもしれません。

一方で Python Fireの良いところは作った関数やクラスが そのままサブコマンド付きコマンドラインツールにできてしまう、というお手軽さが 一番だと思うので、まずなにか作ってみたい、という場合にはPython Fireが一番オススメです。

まとめ

Python Fireを使うと引数の取り回しを自分でやらずに 簡単にサブコマンド付きのコマンドラインツールを作ることが出来ます。

実際、そのあたりを自作しようとするとけっこう大変なので。

ちなみにシェルスクリプトも引数の取り回しを考えるとサブコマンド付きツールというは結構面倒ですが、 Gitみたいに別コマンドを作ってそれをサブコマンドとして登録できる ライブラリみたいなものを以前作りました。

シェルスクリプトでサブコマンド付きツールを作ってみたい場合にはぜひ使ってみてください。

Sponsored Links
Sponsored Links

« Bash 5.0.0リリース PoetryでPython CLIツールを簡単にPyPiに登録する »