LoginSignup
6
5

More than 5 years have passed since last update.

自分が必要とする最低限の Cement の情報

Last updated at Posted at 2015-09-12

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。あと必須の引数が一つ。任意の数指定できる引数。

myapp.py
#-*- 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 では引数が不要であることに注意する。helloworldはいずれも名前空間 sub のサブコマンドであり引数ではない。

myapp2.py
#-*- 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.FileTypedefault=sys.stdinを組み合わせるとスマートに書ける。nargs="?"でオプショナルな引数にできる。

cement3.py
#-*- 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ハンドラやプラグインシステムの開発方式、出力をカスタマイズする方法なども用意されていて、本格的なでかいアプリ開発にも十分使える。が、シンプルなツールを統一された方式で開発するためのよいライブラリなので小規模なところからじゃんじゃん使っていけると思う。
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5