What's Cement
python のコマンドラインアプリケーションフレームワーク。セメント。日本語情報があんまりないので自分が後でググる用にメモしておく。
「A Framework, for the CLI Ninja.」って書いてる。クールなものは全部忍者か。
インストール
pipでインストールできます。
pip install cement
コマンドラインアプリケーションの開発
シンプルなアプリケーション
こんな感じのやつ。単一機能のツールを作るならこのレベルのテンプレートがあれば十分使える。
myapp1 -h
myapp1 --option <オプション>
myapp1 --option -F <引数>
myapp1 --option -F <引数> <省略可能な引数>
myapp1 --option -F <引数> <省略可能な引数> <省略可能な引数> ....
コントローラ一つ。個別に定義するオプションは -F
と--option
。あと必須の引数が一つ。任意の数指定できる引数。
#-*- coding:utf-8 -*-
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
class BaseController(CementBaseController):
class Meta:
label = 'base'
description = "このコマンドの説明だよ"
arguments = [
( ['-o', '--option'],
dict(action='store', default="default option value",help='optionを指定するよ') ),
( ['-F'],
dict(action='store_true', help='大文字Fオプションだよ') ),
(['param1'], dict(action='store', nargs=1, help = "第一引数だよ")),
(['param2'], dict(action='store', nargs="*", metavar="PARAM2", help = "第二引数だよ", default = ["default ext value"])),
]
@expose(hide=True)
def default(self):
self.app.log.debug("デフォルト処理 - 開始")
if self.app.pargs.option:
print "option で指定されたパラメータは <%s>" % self.app.pargs.option
if self.app.pargs.F:
print "F オプションが指定されたよ"
if self.app.pargs.param1:
print "引数 : %s" % self.app.pargs.param1[0]
if self.app.pargs.param2:
print "引数 : %s" % self.app.pargs.param2[0]
self.app.log.info("デフォルト処理")
self.app.log.debug("デフォルト処理 - 終了")
class App(CementApp):
class Meta:
label = 'app'
base_controller = 'base'
handlers = [BaseController]
with App() as app:
app.run()
サブコマンド(subcommand)
コントローラでは @exporse()
をつけたメソッドがサブコマンドとして解釈される。default
メソッドがサブコマンドを省略した際に呼ばれるメソッドである。
@expose(aliases=["y!", "sb"], help="default メソッドの説明")
def yahoo(self):
self.app.log.info("yahoo 処理")
@expose(hide=True)
def default(self):
self.app.log.info("デフォルトの処理だよ")
サブコマンドとかポジション引数は使いどころが難しい。なぜなら引数とかオプションの設定がコントローラ単位だし、サブコマンドと引数がぶつかるとサブコマンドとして解釈されちゃうからだ。たとえば上の例だと、サブコマンドを省略して第一引数として default
とかyahoo
を渡そうとしてもうまくいかない。サブコマンドとして解釈されてしまう。
これはサブコマンド式のCLIを設計するとどうしても直面するので仕方がないところではある。
ちなみに同じコントローラでは引数の定義は共通である。だから、必須引数を一つ持つコントローラがあった場合、その中では引数を持たないサブコマンドは定義できない。
そういう設計をしたい場合、後述の Namespace (ネストしたコントローラ) を利用する。
名前空間(Namespace)
Namespace としてコントローラをネストできる。Example - Multiple Stacked Controllers がわかりやすい。
以下のようなコマンド体系を作る。
myapp2.py <引数>
myapp2.py sub
myapp2.py sub hello
myapp2.py sub world
素の myapp2.py の呼び出し(つまり MainController
) では第一引数が必須だが、名前空間 sub
では引数が不要であることに注意する。hello
とworld
はいずれも名前空間 sub
のサブコマンドであり引数ではない。
#-*- coding:utf-8 -*-
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
class BaseController(CementBaseController):
class Meta:
label = 'base'
description = "ベースコマンドの説明だよ"
class MainController(CementBaseController):
class Meta:
label = 'main'
description = "メインコントローラの説明だよ"
stacked_on = 'base'
stacked_type = 'embedded'
arguments = [
(['param1'], dict(action='store', nargs=1, help="必須の第一引数"))
]
@expose(hide=True)
def default(self):
self.app.log.debug("デフォルト処理 - 開始")
print "引数 : %s" % self.app.pargs.param1[0]
self.app.log.info("デフォルト処理")
self.app.log.debug("デフォルト処理 - 終了")
class SubController(CementBaseController):
class Meta:
label = 'sub'
description = "サブコントローラの説明だよ"
stacked_on = 'base'
stacked_type = 'nested'
arguments = [ ]
@expose(hide=True)
def default(self):
self.app.log.info("サブコントローラの処理")
@expose()
def hello(self):
self.app.log.info("hello world")
@expose()
def world(self):
self.app.log.info("the world")
class App(CementApp):
class Meta:
label = 'app'
base_controller = 'base'
handlers = [BaseController, MainController, SubController]
with App() as app:
app.run()
まあしかしコマンドの利用側からするとどう考えても sub
はサブコマンドだよなぁ。
任意のステータスコードを返す
app.exit_code に値を代入しておけばよい。
@expose(hide=True)
def default(self):
self.app.log.error('まだ実装されていません')
self.app.exit_code = 1
うっかりサブコマンドの戻り値がコマンド全体の戻り値になりそうな気持ちになったりするので注意すること。
パイプで入力を受け取る
パイプとファイル指定、どちらでも動作するようにする。
cement3.py hello.txt
cat hello.txt | cement3.py
argparse.FileType
とdefault=sys.stdin
を組み合わせるとスマートに書ける。nargs="?"でオプショナルな引数にできる。
#-*- coding:utf-8 -*-
from cement.core.foundation import CementApp
from cement.core.controller import CementBaseController, expose
from cement.core import handler
import argparse
import sys
class BaseController(CementBaseController):
class Meta:
label = 'base'
description = "このコマンドの説明だよ"
arguments = [
(["input"], dict(nargs='?', type=argparse.FileType('r'), default=sys.stdin ))
]
@expose(hide=True)
def default(self):
self.app.log.debug("デフォルト処理 - 開始")
for line in self.app.pargs.input:
print ">>> %s" % line
self.app.log.debug("デフォルト処理 - 終了")
class App(CementApp):
class Meta:
label = 'app'
base_controller = 'base'
handlers = [BaseController]
with App() as app:
app.run()
まとめ
- cement 相当クールな設計になってるので使い倒してかっこいいCLIを作ろう。
- 拡張機能としてデーモン化とかサポートされてる。
- 引数の処理は argparse が割とそのまま使われているのでそちらを参考に。
- conf ファイルからの設定の読み出しをサポートしている。
- memcachedハンドラやプラグインシステムの開発方式、出力をカスタマイズする方法なども用意されていて、本格的なでかいアプリ開発にも十分使える。が、シンプルなツールを統一された方式で開発するためのよいライブラリなので小規模なところからじゃんじゃん使っていけると思う。