このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

Catyスクリプト:リリースしたからチュートリアル

あっ、と忘れていたよ。

「Catyスクリプト:まだ出来てないけどチュートリアル」

ソフトウェアを公開してない今の段階で解説を書いても、実感がなくてツマンナイでしょうが、後でまとめて書くことも負担なので、書けることは書いておくことにします。

一応ソフトウェアを公開したので、動く実物を引用して説明できるのだった。

Caty/Python Prototype-1 version 0.1.1 をインストールして、caty.py を実行してください。caty.pyはCatyスクリプトコマンドラインインタプリタを起動します。以下では、実際の操作例で説明します。基本的概念は「Catyスクリプト:まだ出来てないけどチュートリアル」を参照してください。

内容:

  1. JSONデータはそのまま使えるよ
  2. コマンドを実行してみよう
  3. コマンドをパイプラインでつないでみる
  4. コマンドを作ってみよう
  5. こんなコマンドもあります

JSONデータはそのまま使えるよ

すべてのJSONデータは、Catyスクリプトリテラルになっています。例えば、次のデータはそのままCatyスクリプトの式です。

  1. 3.14
  2. "hello"
  3. [3.14, "hello"]
  4. {"PI":3.14, "greeting":"hello"}

リテラルを評価した値はそれ自身になるので、プロンプトに対してJSONデータを打ち込むとエコーされます。ただし、キー入力した文字列がそのままエコーされるのではなくて、式を評価した結果がプリティプリントされるのです。


caty> 3.14
3.14
caty> "hello"
"hello"
caty> [3.14, "hello"]
[3.14, "hello"]
caty> {"PI":3.14, "greeting":"hello"}
{
"PI":3.14,
"greeting":"hello"
}

caty>

キー入力の途中で改行キー*1を打つと、行の継続ができます。(僕は長くても1行で書いちゃう人だけど。)


caty> [
> 3.14,
> "hello"]
[3.14, "hello"]
caty>

純粋なJSONに比べて次の点が便利(あるいは不純)になっています*2

  1. /* ... */ と // ... でコメントが入れられる。
  2. 配列、オブジェクトの最後に余分なコンマを入れてもエラーにならない。


caty> { // start object
> "PI": /* 円周率 */ 3.14,
> "greeting": "hello", // 挨拶
> // 余分なコンマがあるよ
> }
{
"PI":3.14,
"greeting":"hello"
}

caty>

コマンドを実行してみよう

hello, helloJaという、お決まりのつまらないコマンドがあります。


caty> hello
"hello"
caty> helloJa
"こんにちは"
caty>

コマンドのソースは、CATY_HOME/webapp/commands/ の下にあります。

# command hello :: void -> string
class Hello(Command):
    def execute(self, opts):
        self.output = 'hello'

# command helloJa :: void -> string
class HelloJa(Command):
    def execute(self, opts):
        self.output = u"こんにちは"

簡単なPythonコードですね。Catyシェル(caty.py)は、コマンドの名前、仕様、実装を知るために、CATY_HOME/webapp/schema/ の下*3にあるスキーマモジュール(拡張子".casm"のファイル)を読みます。helloとhelloJaはそれぞれ、CATY_HOME/webapp/schema/util.casm と CATY_HOME/webapp/schema/public.casm で宣言されています。別々なファイルで宣言されていることに意味はありません(たまたまそうなっているだけ)。


command hello :: void -> string
refers python:util.Hello;


command helloJa :: void -> string
refers python:util.HelloJa;

宣言を追加・変更すれば、同じコマンド(同一の実装を持つコマンド)を別な名前で呼ぶこともできます。例えば:


command kon-nichiwa :: void -> string
refers python:util.HelloJa;

コマンドのソースや宣言を追加・変更したときはcaty.pyの再起動が必要です*4


caty> kon-nichiwa
"こんにちは"
caty>

version 0.1.1では、コマンドをどのスキーマモジュールで宣言しても効果は同じなんですが、これは変更されます。public.casm以外で宣言されたコマンドは、モジュール名で修飾しないと呼べなくなります。util.casm で宣言されたhelloは、util:hello という形になります。

定数が書けるところにはコマンド呼び出しが書けるので、コマンドの出力からJSONデータを組み立てることができます。


caty> [hello, helloJa]
["hello", "こんにちは"]
caty> {"en":hello, "ja":helloJa}
{
"en":"hello",
"ja":"こんにちは"
}

caty> {"en":hello, "ja":helloJa, "en-ja":[hello, helloJa]}
{
"en":"hello",
"en-ja":["hello", "こんにちは"],
"ja":"こんにちは"
}

caty>

コマンドをパイプラインでつないでみる

文字列の大文字化toupperと、小文字化tolowerというコマンドがあります。これらを使ってパイプラインを試してみましょう。


caty> hello | toupper
"HELLO"
caty> hello | tolower
"hello"
caty> "HelloWorld" | [toupper, tolower]
["HELLOWORLD", "helloworld"]
caty>

コマンドの配列に対しては、入力が項目(item)分だけコピーされて渡されます。コマンドがオブジェクトのプロパティになっているときも同じです。以下で、passは何もしないコマンドです。


caty> "HelloWorld" | {"orig":pass, "upper":toupper, "lower":tolower}
{
"lower":"helloworld",
"orig":"HelloWorld",
"upper":"HELLOWORLD"
}

caty>

配列のi番目を取り出すには、nth i というコマンド、オブジェクトのpという名前のプロパティを取り出すには getpv p というコマンドを使います。


caty> [0, 1, 2] | nth 0
0
caty> [0, 1, 2] | nth 2
2
caty> {"a":"first", "b":"second"} | getpv a
"first"
caty> {"a":"first", "b":"second"} | getpv b
"second"
caty> {"a":["first-0", "first-1"], "b":"second"} | getpv a | nth 1
"first-1"
caty>

これらの基本コマンドも、commands/util.pyでとりあえず定義しただけのモノなので、仕様変更されるかもしれません*5

パイプラインはいくらでもつなげられます。


caty> hello | [toupper, tolower] | [nth 1, nth 0]
["hello", "HELLO"]
caty> "HelloWorld" | toupper | {"a": pass, "b":tolower}
{
"a":"HELLOWORLD",
"b":"helloworld"
}

caty>

コマンドを作ってみよう

「Catyスクリプト:まだ出来てないけどチュートリアル」に:

todayというコマンドは、次のような型のデータを返します。


object {
"year" : integer,
"month" : integer,
"day" : integer
}

実際、todayというコマンドがありますが、プロパティがintegerじゃなくてstringのオブジェクトを出力します。


caty> today
{
"day":"08",
"month":"10",
"year":"2009"
}

caty>

なぜかtodayはシェルの組み込みコマンドになっています(次のリリースでは組み込みから外されます)。todayコマンドのソースは CATY_HOME/python/caty/shell/script/builtin.py にあります。これを参考に、以前の説明どおり、integerからなるオブジェクトを出力するコマンドを作ってみましょう。

ユーザー定義のコマンドは CATY_HOME/webapp/commands/ の下にいれます。CATY_HOME/webapp/commands/mycmd.py というファイルを作って次の内容にします。

# -*- coding: utf-8 -*- 
# mycmd.py

from caty.shell.script import Command
import datetime

class Today(Command):
    def execute(self, opts):
        n = datetime.datetime.now()
        y =  n.year
        m =  n.month
        d =  n.day
        self.output = {'year': y, 'month': m, 'day': d}

これでコマンド実装は出来上がりです。しかし、コマンドはどんな名前でどんな引数を渡すべきかなどの情報が別に必要なので、CATY_HOME/webapp/schema/mycmd.casm に次のように書いてください。


/* mycmd.casm */

type YMD = object {
"year" : integer,
"month" : integer,
"day" : integer
};

command mytoday :: void -> YMD
refers python:mycmd.Today;

Catyシェルを再起動すると、定義したコマンドが有効になります。


caty> mytoday
{
"day":8,
"month":10,
"year":2009
}

caty>

なお、次のリーリスでは、mycmd:mytoday としてコマンドを指定する必要があります。モジュール名mycmdで修飾するのがイヤならば、public.casmにコマンド宣言を書きます。public.casmで宣言されたコマンドは修飾なしの名前を持ちます。

こんなコマンドもあります

最後に、Catyシステムを管理するコマンドをひとつ紹介しておきます。組み込みコマンドadduserです。adduserは、コマンドラインから3つの引数を取ることができますが、Webからコマンドを実行するときにどうするかを見るために、標準入力(input)から必要なデータを流し込んでみます。流し込むJSONデータは、 {"username":"hiyama", "grant":"admin", "password": "foobar"} です。


caty> {"username":"hiyama", "grant":"admin",
> "password":"foobar"} | adduser
true
caty>

これで、管理者(admin)権限を持ったhiyamaというユーザーが追加されました。ユーザー情報の実体は CATY_HOME/webapp/admin/users.json にあります(場所とファイル名は変更されるかもしれません)。

HTMLフォームによるPOSTデータは、Catyへの入り口でJSONデータに変換され、コマンド・パイプラインに流される仕掛けになっています。シェルやコマンドは、コマンドラインがキーボードから来たのかWebから来たのか区別できません。したがって、Webから出来ることはすべてキーボードからもできます(逆は成立しません!)。コマンドラインの実行結果が外に出ていくときは、通常テンプレートにより整形されます。テンプレートエンジンはシェルに内蔵されています -- Catyは「構造的には対話的言語処理系、用途がWebフレームワーク」とは、こういう意味です。

*1:Enter以外に、Ctrl+M でもいいはずですが、Windowsではreadlineがバグっているようです。

*2:次のリリースで入るタギング演算子も含めると、リテラル構文としてもだいぶ良いよ。

*3:次のリリースでは複数形 schemata/ になる予定。

*4:そのうち、reloadコマンドがサポートされるでしょう。

*5:nthは番号を1からにして、別にitemを設けようかと思っています。それと、getpvは頻繁に使うんで pv にしようかと。