あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

Chromebook (Acer C720) 買った

年末に Chromebook (Acer C720) を買いました。

いろいろ弄る中で知見を得たので、記事にすることにします。

結果的に良い買い物だったという気がしています。

購入前

今使っている Macbook Pro も数年使っていて、ハード的にも環境的にもボロボロになってきたので、買い替えを検討していました。

本当は Macbook Air が欲しかったんですけど、結婚してから自由に使えるお金が限られるようになったので、Macbook Air はヨイショと簡単に買えるような端末ではなくなってきていました。

振り返ると、家で使う個人端末では Chrome とシェルしか使ってないので、別に Macbook Air にこだわることはないのかなと思い、安い Linux ラップトップを買おうかなと思ってたんですけど、予てから興味のあった Chromebook を調べてみると、Chromebook で事足るのかなと思い始めてました。Chromebook は、貧乏人の Macbook Air だという評価も見かけましたし。

正直 Chromebook を弄りたいという欲が先行していて、同スペックのより安い Linux ラップトップがあるかどうかはろくに調べていないのですが、Windows 8.1 with Bing なラップトップと比べても、Chromebook はコスパが優れている方な気がします。

自分の日本語入力方式が JIS かな入力なので、JIS 配列のキーボードが必要だと思っていて、輸入で買う選択肢はなく、日本での JIS 配列キーボード搭載版の Chromebook の発売を待っていました。

とりあえず、日本版のすべての Chromebook のラインナップが出揃ったので、本格的に購入することを検討し、一番コスパが良さそうな Acer C720 の購入に至った次第です。

Acer C720 は現時点で日本で発売されているラインナップのうち、唯一 SSD を換装できる端末らしいのですが、SSD は 16GB のまま換装はしませんでした。(後述しますが、16GB では余裕で足らないです)

Web 開発できることが条件

Chromebook を安物買いの銭失いにするのは絶対に避けたかったのと*1、今後のメインマシンにするつもりで購入するので、Web 開発できることが絶対条件でした。

Chromebook に Ubuntu を入れる方法はすでに確立されていて、それでなんとかなるかなと思っていましたが、実際に使ってみて今のところなんとかなりそうな気配です。

Chrome OS に関しては、Virtualbox にインストールして触ってみてましたが、Chrome に毛が生えた程度なので、特に事前に触ってみる必要もなかった気もしています。

OS として起動が速い点は大変良いです。*2

Web 開発環境のセットアップ

Ubuntu は crouton でインストールしました。

他にも選択肢はあるようですが、Issue 見てもちゃんと対応しているし、複数人でメンテされていて、crouton であれば、Chrome OS の特徴を殺すこともないので、自分的には crouton しか選択肢なかった感じです。

crouton は chroot の仕組みを使って、Ubuntu をインストールする仕組みですが、chroot するディレクトリは任意に選べます。

本体の SSD の容量は 16GB で、実際使える容量はさらにそれより少なく、余裕で足らない感じだったので、USB メモリ内のディレクトリにインストールすることにしました。

幸い、Chromebook には、USB 3.0 端子があるので、高速な USB メモリが利用できます。

ネットで調べて評判も高そうだったサンディスクの 64 GB の USB メモリを買いました。

64 GB あればとりあえず当面は十分です。

IO の反応も非常に速く、リムーバブルディスク上で操作している感はありませんでした。SSD すら初体験という前提はありますが。

USB メモリを ext4 でフォーマットしようと思ったところ、USB 3.0 に対応した端末が Chromebook しかなく、詰んだかなと思ったんですけど、普通に Chromebook の Crosh ウィンドウのシェル*3で、fdisk やら、mkfs.ext4 が使えたので、事無きを得ました。

ついでに、USB のラベルも、接続直後、「USB Disk」という微妙なラベルだったので、e2labelchromebook に変更しました。(以下、USB メモリのラベルを chromebook に変更していることを前提としてパス等記述しています)

crouton のインストールオプションは下記のような感じです。

sudo sh ~/Downloads/crouton -r trusty -t xfce -p /media/removable/chromebook/crouton

t オプションに xiwi を指定して、crouton の Chrome エクステンションをインストールすると、タブ上で Ubuntu を開くことができたり、Chromebook と Ubuntu 間で、コピー・ペーストを共有したりできるらしいのですが、タブ上で Ubuntu を開くのはうまくできませんでした。

あと、cli-extra というターゲットを指定すると、X Window System のないミニマムな Ubuntu 環境をインストールできますが、後述する通り、Virtualbox 使うために X Window System が必要だったので、指定しませんでした。

インストールが終わったら Ubuntu が USB メモリに入ります。USB メモリがなければただの Chromebook で、USB メモリがあれば Ubuntu も利用できるという感じで、Chromebook はそのまま残しつつの運用ができるのでおすすめです。

p オプションをつけてインストールすると、インストール先の bin ディレクトリ(例えば上記で言えば、/media/removable/chromebook/crouton/bin)に crouton の各種スクリプトがインストールされます。

Crosh ウィンドウのシェルで、

sudo sh /media/removable/chromebook/crouton/bin/startxfce4

と入力すれば、Xfce4 の Ubuntu デスクトップ環境が起動しますし、

sudo sh /media/removable/chromebook/crouton/bin/enter-chroot

と入力すれば、Crosh ウィンドウでそのまま UbuntuCUI 環境で利用できます。

UbuntuSSH サーバー入れて、自動で起動する設定をしておいて、Secure ShellSSH ログインするという方法もありますが、sudo enter-chroot で Crosh ウィンドウからそのまま UbuntuCUI 環境使う方が 、Ubuntu デスクトップ環境を起動する必要もなく、気軽だと思います。

VirtualboxWindowsIE)を動かせるようにする

Chromebook で Web 開発するにあたって、VirtualboxWindowsIE)を動かせることも必要条件と考えていました。

インストールした Ubuntu をデスクトップ環境としてそのまま利用することは考えておらず、Virtualbox を利用するためのデスクトップ環境という感じで考えています。*4

ただ、Virtualbox のインストールは簡単ではありませんでした。

crouton リポジトリWiki を参照して、

あたりを見ながら各種設定し、modern.IE の OVA で、Windows 7IE 11 が動かせたので、この条件もクリアできました。

カーネルヘッダのビルドは手動でもやってみましたが、すごく時間がかかって大変だったので、上記の Wiki にリンクのある、下記のスクリプト使うのが良いかなと思います。自分の環境はそれで動きました。

Virtualbox のインストールは何度か躓いて、躓いた時に何が足りなかったのかイマイチわかってない状況です。Xfce4 ではなく、Unity をインストールした時に、crouton の Issue にも同例の報告があった Virtualbox で Windows 起動した時に Chromebook が強制的にリブートする問題が発生してうまくいかなかったんですけど、Xfce4 に変更した今は問題は発生していません。ネットで情報を見ながらアレコレ試行していたので、この問題がウィンドウマネージャを変更したことで解決したのかどうか、それが関係あるのかないのかも正直よくわかってません。。。少なくとも Issue にあるように Trusty を Precise に変更するという方法では解決しませんでした。

Virtualbox がインストールできたことで、他の OS を操作することができる状況も確立したし、Chrome リモートデスクトップ を使って、既存の Macbook Pro を操作できることも確認できたので、今後困ったことがあっても逃げ道ができた感じです。

あとは日本語言語パックの設定とロケールの変更、シェル環境の整備、LAMP 周りのソフトウェアとか入れて、いい感じに使えるようになりました。

とにかく Virtualbox のインストールだけが一癖あって、他のツールのインストールは特に問題は起こっていません。

各種環境を整えた後、Movable Type をセットアップして実際に操作してみましたが、IO が速いせいか、既存の Macbook Pro 上の環境と比べても速く、今のところスペックによるストレスは感じていません。

困った点と解決方法

以上のセットアップで、Chromebook で Web 開発できるような雰囲気になりましたが、いろいろと細かいところで問題があり、これからもあるかもしれません。

使ってみて発覚した各種の問題を、自分はどのように解決したか、または解決するつもりなのかということを、今後購入される方向けに残しておきます。

Crosh ウィンドウが複数開けない

Crosh ウィンドウの欠点としては、複数ウィンドウが開けない点です。

一方、Secure Shell は複数ウィンドウが開けます。

Ctrl + Alt + TChrome のタブに複数開くこともできるのですが、Chrome のタブとして開くと、Chrome のショートカットキーが優先され、一部のキー操作が奪われるという問題があります。

Secure Shell であれば、ウィンドウを複数開くことができ、かつ、それらを Chrome のタブとしてではなく、独立したウィンドウとして開く設定にしておけば、Chrome のショートカットキーにキー操作が奪われることもないので、この運用を好む方もいるかもしれません。

ただし、Secure Shell で開く場合は、Crosh ウィンドウから Ubuntu デスクトップを起動しておく必要があります。

cli-extra を入れて、sudo startcli するという方法もありますが、シェルが別画面に開くだけで、使用感は変わりませんでした。

自分は、Crosh ウィンドウ内で sudo enter-chroot して UbuntuCUI 環境で開き、複数ウィンドウ開けない問題については、tmux や screen を使うことで対応することにしました。

日本語情報が少ない

まだ、日本語情報が少ない感じです。ただ、すでに欧米では Chromebook のハックの実績が多く、知見もあるので、英語圏の情報は充実しています。

しばらくは、何か調べる際は、英語圏の情報を探すしかないです。

crouton 固有の情報は、経験的に、crouton リポジトリの Issue や Wiki から探すのが良いです。

セットアップのやり直しをするたびユーザー領域がリストアされる

個人的にもセットアップのやり直しを何度か実施したのですが、そのたびにユーザー領域がリストアされます。

設定情報等は、クラウドに保存されるようで、再セットアップの手間はそれほどではありません。

消えて困るデータは、リムーバブルメディアや Google Drive に保存するようにした方が良さそうです。

/etc/hosts の書き換えができない

Ubuntu 側は問題ありませんが、Chromebook 側の /etc/hosts の書き換えができません。*5

DNS サーバーをセットアップすることで任意のホストで Ubuntu 上のウェブサーバーのリソースにアクセスできるようにすることも可能なのかもしれませんが、自分は xip.io のようなサービス使って凌ぐことにしました。

サービス(xip.io)が終了したら、任意のドメインで同じようなことを適当にやります。

Secure Shell でも Crosh ウィンドウのシェルでも日本語入力できない

ここ重要!

Chrome で Secure Shell 使う時もこの問題があったので、予想はしてましたが、Chromebook の Crosh ウィンドウや Secure Shell であっても同じ状況です。コピペはできますが、それでは厳しいです。

Chromebook にインストールした Ubuntu デスクトップ環境を、そのまま利用する場合は、問題なく日本語入力できます。

Crosh シェルで crouton の enter-chroot コマンドで CUI 環境を利用する場合や、Secure Shell で SSH ログインして CUI 環境を利用する場合には、ウィンドウ内で日本語入力ができないので、この点が購入にあたっての大きな障害になり得ます。(Vim で開発する人とか)

自分の場合は、Chrome で Secure Shell の操作を確認した際に、SSH ログインした環境で、Emacs の入力切替ができることは確認できていたので、Vim から Evil + Emacs に移行しつつある自分的にはクリアできる問題となりました。

要するに日本語入力したければ Emacs 使いましょうという話です。

mozc.el 入れて、通常と同様に文字変換できるし、設定を変更すれば、JIS かな入力もできます。

シェル上で日本語を使いたい場合も、eshell 使えば良いので、無問題でした。

Vim を使っている時にハングアップする

よくわかりませんが、Crosh ウィンドウや、Secure Shell のウィンドウで、Vim を使っていると、突如ハングアップして、 使っていたウィンドウがどれであるかは関係なく、シェルを起動したすべてのウィンドウが軒並み動作しなくなることが何度かありました。

その状態では新しくシェルを立ち上げることもできないので、再起動するしかなくなります。

Emacs では今のところ発生していないので、基本は Emacs を使っています。

これはハード固有の問題なのか、たまたま自分の環境で起こっている問題なのか、解決策があるのかもわかっていません。

ハード固有の問題

Acer C720 では、リストア後のセットアップで WiFi に繋がらないという問題が、2回ほど発生しました。

一度接続設定をしてからは問題ないのですが、リストア後のログイン直後でそういう事象が起こりました。

1回目は何度も試行しているうちにつながり、2回目はルータを再起動したらつながりました。

多分、下記の QA と同様の事象だと思いますが、何が起こっていたのかよくわかりません。

あとは、キーボードやディスプレイ*6、タッチパッドが値段相応の品質ということです。これはどうしようもないので、慣れるしかない。

Macbook がよく出来ているということを痛感した感はあります。

まとめ

Macbook Air を購入しても財布が痛くない人は、素直に Macbook Air 買えば良いと思います。。。

自分の場合は、型が古くなっても新しい端末が買えなかったり、故障した時に新しい端末の調達がすぐにできないといった状況がストレスなので、今後そのようなことがあっても安く必要十分なマシンが調達できることが確認できて良かったです。*7

また、crouton でインストールした UbuntuCUI 環境に、Crosh ウィンドウのシェルからアクセスする運用は、Chrome OS をシャレオツな UNIX デスクトップ環境として使っている感覚で気に入りました。*8

これで今後も持たざる者(いろんな意味で)として快適に生きられそうです。

ただ、今のところ万人におすすめできるものとは思ってません。特に日本語入力できない問題が痛い。

あ、あけましておめでとうございます。

ほなの。

追記

最近の運用方法書きました。

taiju.hatenablog.com

*1:それだとコスト削減のために Chromebook 買う意味がなくなるし。

*2:あと、バッテリーがやたら持ちます。外出ないけど。

*3:Crosh ウィンドウを開いて shell とタイプして利用できるシェル。

*4:インストールした UbuntuChromium を起動した時の敗北感たるや...

*5:調べるとやりようはあるっぽいのですが、やってません。

*6:HDMI 端子があるので、テレビに繋げるという選択肢もある。多分使わないけど。

*7:少なくとも Chromebook が発売され続ける間は。

*8:crouton ありきな危うい状況ではありますが。

MT の管理画面を開くコマンドラインランチャー作った

この記事は、Movable Type Advent Calendar 2014 の 21 日目の記事です。

最近になって、遅ればせながら peco を使い始めました。汎用的なインタラクティブフィルタリングツールなので、どんなコマンドとも組み合わせて使うことができるし、ないと困るツールになりそうな気配です。

本題ですが、MT の特定の画面をサクッと開けるランチャーを作りたいなと以前から思っていたんですけど、一から考えて作るのは厳しいのでほぼボツ案だったのですが、peco または percol を使えばそれなりに満足するものが作れそうだなと思ったので、昨日作ってみました。

MT::Tool::Launcher

$ cd $MT_HOME
$ ./tools/launcher --protocol=https --host=cms.example.com --cmd=/path/to/peco

実行例

例1) あるウェブサイトの記事作成画面を開く

https://github.com/taiju/mt-tool-launcher/raw/master/artwork/launcher1.gif

例2) すべてのブログのプラグイン設定画面を開く

https://github.com/taiju/mt-tool-launcher/raw/master/artwork/launcher1.gif

上記のような感じで使います。

所感

ローカルマシンに環境作って開発してるケース以外では使うことないと思いますが、自分はローカルマシンで開発することが多いので、あると便利かなと思っています。

一応 Windows でも動くように作ったつもりなんですけど、手元に検証に使える環境がないので動かなかったらごめんなさい。MacUbuntu では動作確認してます。

peco や percol は汎用的なツールなので、他のツールスクリプトにもいろいろ応用ききそうで、発想が広がる感じがします。

ということで 21 日目の記事でした。引き続きお楽しみに。

ほなの。

MTの管理画面でMT標準のJSテンプレートエンジンを使う

管理画面のカスタマイズをしていて、JSでDOMを弄ってHTMLを出力しようと思った時に、コードの見通しを確保するためにJSテンプレートエンジンが欲しいなと思いました。

ただ、そのためにライブラリ読み込むのも微妙だし、正規表現で頑張るかと思ったところ、管理画面でJSテンプレートエンジンが使われてたことを思い出しました。

MTの管理画面で使われている、JSテンプレートエンジンのライブラリは、/path/to/mt-static/js/common/Template.js にあって、実装を確認したところ、シンプルで汎用的に使える小さなライブラリっぽいので、下記の通り機能をまとめました。

ちなみにこのライブラリは、数年前から更新が止まっていて、Github リポジトリの当該ファイルのコミット履歴も、「Common JavaScript ライブラリをリポジトリに入れました」的なログが一つあるだけのレベルです。

メンテが止まっていて、いつ役目を終えるかわからない感じの存在感なので、その点はご留意ください。

この記事の賞味期限もそれまでです。

使い方

下記のような感じで使えます。

var tmpl = new Template('Hello, [#= name #]!');
var context = new Template.Context({ name: 'taiju' });
tmpl.process(context);
// => "Hello, taiju!"

/*
下記で使う変数tmpl_textには下記のテンプレート文字列が保存されていると仮定する
  <ul>
  [# jQuery.each(vars, function (i, v) { #]
    <li>[#|h|u v#]</li>
  [# }) #]
  </ul>
*/
tmpl.compile(tmpl_text);
context.vars = { vars: ["<script>alert('foo');</script>", "バー", "baz"] };
tmpl.process(context);
/* =>
"<ul>

  <li>&lt;script&gt;alert('foo');&lt;%2Fscript&gt;</li>

  <li>%E3%83%90%E3%83%BC</li>

  <li>baz</li>

</ul>"*/

Underscore.jsのテンプレート機能のように、普通に言語の機能がそのまま使えるタイプのシンプルなテンプレートエンジンです。この手のテンプレートは学習コストが低く、言語のパワーがそのまま使えるので、個人的に好むタイプです。

管理画面はjQueryがロードされているので、jQueryのユーティリティを組み合わせてテンプレートを書くのが良いかもしれません。

ライブラリの機能

Template.jsによって、Template, Template.Context, Template.Filterというクラスが提供されます。

Template.jsは、Core.jsに依存しているので、利用するにはCore.js等の依存ライブラリがあらかじめ読み込まれている必要があります。

主に使うのはTemplateクラスになりそうですが、先のサンプルのようにTemplateクラスのインスタンスを作って処理する他、Templateクラスのクラスメソッドを使って、JSテンプレートをビルドすることもできます。

var templates = {
  foo: '[#= foo #]'
};
var vars = {
  foo: 'FOO'
};
Template.process('foo', vars, templates);
// => "FOO"

クラスメソッドの方が手軽に使えるかもしれません。

テンプレートの記法

[#から#]で囲まれた部分が JS テンプレートの記述部分になります。

JavaScript の任意の式や文が記述でき、式の結果を出力したり、出力結果にフィルタをかけたりできます。また、Template.ContextのインスタンスをTemplateクラスのインスタンス(compile済み)の持つ、processメソッドに渡すことで、テンプレートに任意の変数を渡すことができます。

[##]というテンプレートの開始位置、終了位置を指定するための文字列は、TemplateクラスのインスタンスbeginTokenプロパティとendTokenプロパティを変更することで変更可能です。

頻繁に使うことになるのは、[# ... #] と [#= ... #] ですかね。テンプレートエンジンでよく見かける形式だと思います。

テンプレートのサンプル

[# ... #]

[# if (false) { #]
ここは出力されない
[# } #]

[#から#]で囲まれた範囲では、任意のJavaScriptの式や文を評価することができます。評価内容は出力されません。

[#-- ... --#]

[#-- 下記でゴニョゴニョする --#]
ゴニョゴニョ

[#--から--#]で囲まれた範囲はコメントになります。コメント中の式や文は実行されず、出力もされません。

[#= ... #]

[#= true ? 1 : 0 #]

[#=から#]で囲まれた範囲は、任意のJavaScriptの式を記述でき、評価された値が出力されます。

[#* ... #]

[#= foo #]
[#* return #]
[#= baz #]<!-- この変数の値は出力されない -->

[#*から#]で囲まれた範囲は、定義された特定のコマンドを記述できます。

とは言え、実際には、returnしか定義されておらず、returnコマンドを記述すると、その記述以降のテンプレートは評価されません。

[#| ... #]

[#|i "foo ${bar} baz" #]<!-- ${foo} や $foo という記述が変数展開される -->
[#|h "<script>alert(1);</script>" #]<!-- HTMLをエスケープする -->
[#|H "&lt;script&gt;alert(1);&lt;/script&gt;" #]<!-- HTMLをアンエスケープする -->
[#|u "テスト" #]<!-- URLエンコードする -->
[#|U "%E3%83%86%E3%82%B9%E3%83%88" #]<!-- URLデコードする -->
[#|lc "ABC" #]<!-- 小文字にする -->
[#|uc "abc" #]<!-- 大文字にする -->
[#|substr(0, 3) "abcdefg" #]<!-- 指定した位置から指定した位置まで文字を切り出す -->
[#|ws "   foo   " #]<!-- 前後の空白を除去する -->
[#|trim(3) "abcdefg" #]<!-- 引数で指定した文字以降を…(U+2026)で省略する -->
[#|date "2014-06-05T00:00:00+09:00" #]<!-- ISO形式の日付文字列をISO形式の年月日文字列に変換する -->
[#|localeDate "2014-06-05T00:00:00+09:00" #]<!-- ISO形式の日付文字列をロケールに合わせた表記に変換する -->
[#|rt "<script>alert(1);</script>" #]<!-- タグを削除する(内容は残す) -->
[#|rp(/foo/g,"bar") "foobarfoobar" #]<!-- 引数に指定した文字を引数に指定した文字に置換する -->

[#|から#]で囲まれた範囲は、評価された式の値に、定義されたフィルタを適用した上で値を出力します。

上記の中でも、iフィルタや、dateフィルタは、内部で使っている関数がCore.jsに依存しています。

MTのJSライブラリあまり調べたことなかったので意識していなかったんですが、結構グローバルオブジェクトを拡張してるようです。

rpフィルタはうまく動いていなかったので、PRしました。

また、フィルタはUnixのパイプのような感じで、複数組み合わせることができます。

[#|ws|rt|uc "   <span>abcd</span>  " #]<!-- 前後の空白を除去してから、タグを除去して、内容を大文字にする -->

Template.Context, Template.Filter について

Template.Contextには、いろいろメソッドが用意されていますが、Templateクラスを通して透過的に利用することがほとんどな気がしたので、紹介は割愛します。フィルタに関しても、基本はテンプレート内に記述するケースがほとんどでしょう。

Template.Contextのincludeメソッドは単体でも使うかもしれません。

var templates = {
  header: 'Header!!',
  body: '[#= context.include("header") #] Body!! [#= context.include("footer") #]',
  footer: 'Footer!!'
};
Template.process('body', {}, templates);
// => "Header!! Body!! Footer!!"

まとめ

よく使う機能に関しては、上記で紹介した通りです。あとはソース読んで確認してください。

昔はいろいろとMT標準のJSライブラリについて、ナレッジが共有されていたのかもしれませんが、今は、ググってもあまり情報が見つからない感じがします。

Template.jsに限らず汎用的に使えそうなライブラリもありそうですが、ドキュメントないし、自分のような新参者はソース読むしかない感じです。公式ドキュメントが待たれるところ。

Template.jsは、汎用的に使えるテンプレートエンジンだと思うので、MTの管理画面のカスタマイズ用途等で、特に他のライブラリを読み込む予定が他にない時は、このJSテンプレートエンジンを使おうかなと思ってます。

ほなの。

PSGI + SeleniumでMTの管理画面のテストをする

MTの管理画面は、JavaScriptに依存した部分が結構多く、PhantomJS等のヘッドレスブラウザを使ったテストが必要になるケースがあります。

CasperJSや、Seleniumなど、いろいろなツールがありますが、MTの管理画面のロジックは、Perlで書かれているので、Perlで書けるのがベストです。Perlでデータの初期化とかしたいですからね。

Perlで書ける方法でツールを絞り込むと、現状では、Selenium::Remote::Driverを使うのが良い気がしています。

ということで、下記のようなテストを書いてみました。

テストの実行には、MTのGitHubリポジトリに含まれる、tディレクトリが必要です。あと、試す場合は環境変数MT_CONFIGを書き換えずに、MT::TestでDBを初期化すると、既存データが吹っ飛ぶので、テスト用の環境は別途用意した方が良いです。

Test::TCPで、GhostDriverと、MTのPSGIの管理画面を空いてるポートで起動して、Selenium::Remote::Driverを使って、管理画面を操作する感じです。

実際書いてみると、いろいろハマりどころがあった感じですが、ハマりどころを回避しつつテストを書くことになりそうです。

テストと同時にキャプチャを撮れるので、エビデンス対策も取れてよいですね。上記のコードではダッシュボードと、記事作成後の画面をキャプチャしていて、下記のような感じのキャプチャが撮れました。JavaScriptも処理されていて、ちゃんとした見た目になってます。

f:id:jdg:20140501090135p:plain

f:id:jdg:20140501090152p:plain

もう少し便利メソッドをカジュアルに使いたい場合は、Wight を使うのも良い気がします。

他言語でもSelenium使う機会があるかもしれないので、今後の学習コスト削減のために、今のところは単純なアダプタであるSelenium::Remote::Driverを使おうと思います。

ほなの。

MTオブジェクトの複製を作るツールスクリプトを作った

Movable Type使ってる時に、たまに、ページングのテストや負荷テスト等をしたい時に、記事を大量にコピーしたい時があって、これまで下記のようなスクリプトを叩いたりしてコピーを作ってました。

これをMTオブジェクト全般に広げても良いかなと思ったので、ObjectCloneというプラグインを作りました。

ObjectClone プラグイン

usage のままですが、下記のように使います。

USAGE: perl tools/object-clone --model=NAME --orig_id=NUM [OPTION]

Requires:
  -m, --model=NAME      Model name (object datasource) of object to make clone.
  -o, --orig_id=NUM     Original object id clone object.

Options:
  -a, --amount=NUM      Amount of clones to make. Default 1.
  -d, --debug           Output debug info to STDERR.
  -h, --help            Show help.
  -r, --redefine=PAIR   Redefine column value with column key and column value pair.
  -u, --usage           Show usage.

Examples:
  # Make clones of 100 objects of MT::Entry from original entry that entry_id is 1.
  $ tools/object-clone --model=entry --orig_id=1 --amount=100

  # Make a clone of MT::Blog, and redefine name, site_path and site_url.
  $ tools/object-clone -m blog -o 2 -r 'name=Clone Blog' -r 'site_path=/path/to/site-path' -r 'site_url=/::/clone-blog/'

基本は、モデル名とコピー元のオブジェクトのIDを、それぞれ、--modelオプションと--orig_id オプションで指定してクローンを作ります。

クローンを複数作る場合は、作る数を--amountオプションで指定します。

他には、単に丸々コピーするだけではなくて、一部、値を変更する場合は、--redefineオプションで、フィールド名とその値を=で区切って指定します。

具体的には、Aというブログの記事を、Bというブログにコピーしたいような時などに、下記のように指定します。

$ perl tools/object-clone --model entry --orig_id 1 --amount 10 --redefine blog_id=3

コマンドラインオプションの単語が適切かどうかは微妙な感じもしますが、とりあえず必要最低限なオプションだけ用意しました。

値の変更が必要なケース

DB上の制約はないにも関わらず、実際にはMT::Entryのベースネームだとか、CustomFields::Fieldのタグ名は、値が重複しないようにCMS上で制限がかかっていたり、バックグラウンドでの値の生成の実装があったりします。

その辺の対応はオブジェクト毎に必要な感じなので、ObjectClone::Patcherとそのサブクラスに切り出しています。

単体で使うことはほぼないと思いますが、下記のような感じで使います。

use strict;
use warnings;

use lib qw(lib extlib plugins/ObjectClone/lib);

use MT;
use ObjectClone::Patcher;
use ObjectClone::Patcher::MT::Entry;

my $mt = MT->instance;
my $orig_obj = MT::Entry->load(1) or die MT::Entry->errstr;
my $new_obj = $orig_obj->clone;
my $patcher = ObjectClone::Patcher->model('entry')->new($new_obj, $orig_obj);
$patcher->remove_patch(basename => \&ObjectClone::Patcher::MT::Entry::basename)
        ->add_patch(basename => sub { 'new-basename' })
        ->add_patch(convert_breaks => sub { 'markdown' })
        ->apply_patch;
$new_obj->save or die $new_obj->errstr;

デフォルトで下記のPatcherクラスを用意しています。

  • ObjectClone::Patcher::MT::Entry
  • ObjectClone::Patcher::MT::Category
  • ObjectClone::Patcher::MT::Author
  • ObjectClone::Patcher::CustomFields::Field

とは言っても実際に実装があるのは、ベースネームをユニークにする処理くらいです。

独自オブジェクトをtools/clone-objectで生成したいときなどは、上記のPatcherクラスを追加すれば対応ができます。( ObjectClone::Patcher::{オブジェクト名} )

後は、apply_patchメソッドで、パッチを適用する前に、before_apply_patchというコールバックが呼ばれるので、デフォルトのパッチ処理を無効にしたり、それに加えて何かの処理を実行したりする時は、コールバックプラグインで拡張できます。

コールバックプラグインのサンプル

下記のようにコールバックを使ったプラグインが書けます。

id: ObjectCloneCallback
key: object-clone-callback
name: ObjectCloneCallback
callbacks:
  ObjectClone::Patcher::MT::Entry::before_apply_patch: |
    sub {
      my ($cb, $patcher) = @_;
      $patcher->add_patch('basename', sub {
        my ($new_obj, $orig_obj) = @_;
        'new-basename';
      });
    }

まとめ

主に開発ツールとして作ったので、開発時には役に立つ気がしています。

シェルスクリプトと組み合わせたりするのも良いかもしれません。

ほなの。

MT::Object をワンライナーしやすくする拡張書いた

コマンドラインで、MT::Objectを取得して、加工して、出力してみたいなことすることが結構あるんですけど、MT::Objectは大変ワンライナーしにくい印象でした。

MTのシステム管理者のIDとパスワードがわからない時、コマンドで強制的に作ることがあるんですけど、MT::Objectがワンライナーしにくいので、下記のようなファイルを用意してから、実行するというまどろっこしさです。

試しにワンライナーしてみると下記のような感じでしょう。

$ perl -I{lib,extlib} -MMT -e 'my $taiju = MT->instance->model("author")->get_by_key({name=>"taiju"});$taiju->nickname("taiju");$taiju->email("higashi@taiju.info");$taiju->auth_type("MT");$taiju->status(1);$taiju->set_password("password");$taiju->is_superuser(1);$taiju->save or $taiju->errstr;printf "You can use taiju (id=%d) (password=password)! taiju is suupppeeeer user!!\n", $taiju->id;'

...セミコロン......

これがワンライナーと言えるなら、minifyされた jQueryですらワンライナーになってしまいますね。

MT::Object::Chaining

ということで作ったのが下記になります。

MT::Object::Chaining - Methods chaining for MT::Object

MT::Object::Chainingを使うと、下記のように書けます。

perlコマンドのように、Iオプションや、Mオプション指定して、インスタンス取得するのすらダルいので、tools/chainというスクリプトでその辺をカバーしてます。

mオプションでモデルを指定して、eオプションでeval文字列を指定します。

$ cd /path/to/mt && tools/chain -m author -e '$model->get_by_key({ name => "taiju" })->nickname("taiju")->email("higashi\@taiju.info")->auth_type("MT")->status(1)->set_password("password")->is_superuser(1)->save->tap(sub { printf "You can use taiju (id=%d) (password=password)! taiju is suupppeeeer user!!\n", shift->id })'

こんな感じでメソッドチェーンで書けるようにする機能を提供するような感じですね。

まとめ

これでMT::Objectをワンライナーしやすくなりました。

jsonで吐くAPIなども用意しているので、

$ tools/chain -m author -e '$model->load->json' | jq '.[] | {name, email}'

みたいな感じでjqに渡したりなど、捗るかもしれません。

また、mapメソッドで一括置換して保存とか、reduceで加工した結果を出力するとか、普段使いに個人的に便利だと思う機能も入れました。

まだ、v0.1なのと、統一感に欠ける気もしているので、インターフェイスの変更するかもしれません。

詳しい使い方は、README.pod を参照下さい。

https://github.com/taiju/MT-Object-Chaining/blob/master/README.pod

ほなの。

MT アプリケーションを Sinatra like に記述できる MT::App::Lite 作った

Movable Type には、MT::App という Web アプリケーションを記述するための基底クラスがありますが、結構慣れが必要なのと癖があることもあって、もっと手軽に Sinatra like にアプリケーションを記述できる、フレームワーク的なものが欲しいなーと常々思っていました。

とりあえずいろいろやりたいことは残っていますが、動くところまでできたので、紹介します。

現状では、PSGI でのみ動作します。

名前

概要

以下のような感じでMTアプリケーションを記述します。

package MyLiteApp;
use strict;

use MT::App::Lite;

setup Renderer => 'Xslate';

get '/' => sub {
  my $app = shift;
  $app->render('index.tt', {
    blog => MT->model('blog')->load(1),
    entries => [MT->model('entry')->load({blog_id => 1})],
  });
};

get '/entry/:id' => sub {
  my $app = shift;
  $app->render('entry.tt', {
    blog => MT->model('blog')->load(1),
    entry => [MT->model('entry')->load($app->param('id'))],
  });
};

1;

__DATA__

@@ index.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $blog.name :></title>
</head>
<body>
  <ul>
  : for $entries -> $entry {
    <li><a href="<: $entry.permalink :>"><: $entry.title :></a></li>
  : }
  </ul>
</body>
</html>

@@ entry.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $entry.title :> | <: $blog.name :></title>
</head>
<body>
  <h1><: $entry.title :></h1>
  <div>
    <: $entry.text | mark_raw :>
  </div>
</body>
</html>

1ファイル完結型です。(正確にはプラグインの config.yaml が必要)
多分、フォームとか作るのに便利だと思います。

仕様

MT アプリケーションのハンドラ内で、Router::Simple::Sinatraish でエクスポートされる、get や post などの関数を使って、ルートを記述します。

そのファイル中の __END__ セクションに、テンプレートを書きます。
__END__ セクションに記述したテンプレートは、Data::Section::Simple によって、MT::App::Lite に渡されます。

テンプレートは、Text::Xslate を標準のテンプレートエンジンにしています。申し訳程度に MTML 用のテンプレートエンジンも用意しましたが、ここでわざわざ使う必要はない気がします。

MT::App::Lite::Renderer::Foo という形で、render メソッドと render_string メソッドを用意すれば、任意のテンプレートエンジンを利用することは可能です。

また、MT::App::Lite を use すると、自動的に MT::App を継承することになるので、MT::App っぽい書き方もできるのかもしれません。

基本的には、MT::App に Web アプリケーションを記述するための機能はひと通り揃っているので、便利っぽいモジュールを糊付けしたような実装です。

MT アプリケーションのプラグインを作成する

このモジュールを使った、MTアプリケーションのプラグインの作り方です。

最小限の config.yaml と、ハンドラを用意します。

config.yaml
name: MyLiteApp
id:   myliteapp

applications:
  lite_app:
    handler: MyLiteApp
    script: sub { 'app' }
    cgi_path: sub { '/' }
MyLiteApp (handler)
package MyLiteApp;
use strict;

use MT::App::Lite;

setup Renderer => 'Xslate';

get '/' => sub {
  my $app = shift;
  $app->render('index.tt', {
    blog => MT->model('blog')->load(1),
    entries => [MT->model('entry')->load({blog_id => 1})],
  });
};

get '/entry/:id' => sub {
  my $app = shift;
  $app->render('entry.tt', {
    blog => MT->model('blog')->load(1),
    entry => [MT->model('entry')->load($app->param('id'))],
  });
};

1;

__DATA__

@@ index.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $blog.name :></title>
</head>
<body>
  <ul>
  : for $entries -> $entry {
    <li><a href="<: $entry.permalink :>"><: $entry.title :></a></li>
  : }
  </ul>
</body>
</html>

@@ entry.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $entry.title :> | <: $blog.name :></title>
</head>
<body>
  <h1><: $entry.title :></h1>
  <div>
    <: $entry.text | mark_raw :>
  </div>
</body>
</html>

これで、http://yourdomain/app/ や、http://yourdomain/app/entry/:id にアクセスするとテンプレートの内容が描画されます。

まとめ

本当は、Movable Type Advent Calendar 2013 の自分担当日のネタとして作ろうと考えていたんですけど、実装する暇がなく、今日に至ります。

便利な気がするので、メンテしていこうと思います。まだいろいろと実装したいことや、実装すべきことがあります。

現時点ではちょっとした Viewer やフォームを作るのには便利な気がしています。