株式会社クリアコード > ククログ

ククログ

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|
タグ:

groongaで高速な位置情報検索

groongaのドキュメントにも位置情報検索について書かれているのですが、情報の更新が追いついていないため情報が不足しています。そこで、ここに現状に合わせたgroongaの位置情報検索についての情報をまとめておきます。なお、ここにまとめた内容もドキュメントに反映させる予定です。

できること

groongaには位置情報を用いた検索機能がついています。位置情報を用いた検索では索引を利用するため、全文検索と同じように高速に検索することができます。ただし、PostGISMySQLのように*1線や面などもデータとして保持できるというわけではなく、点のみをデータとして保持できます。よって、groongaにできることは以下の通りです。

  1. 指定した四角の中に含まれている座標を持つレコードを検索する。
  2. 指定した円の中に含まれている座標を持つレコードを検索する。
  3. 座標間の距離を計算する。
  4. ある座標からの距離が近い順にレコードをソートする。

つまり、以下のようなユースケースにはgroongaの位置情報検索機能を使うことができます。

  • 東京ドーム周辺のコンビニを検索する。
  • 新宿駅東口から近い順に和菓子屋をソートして表示する。
  • 今いる場所の近くにあるラーメン屋をリストアップして、近い順にソートして、今いる位置からの距離も計算する。

一方、以下のようなユースケースでは使えません。

  • 杉並区内にある駅を検索する。(1つの四角または1つの円で表現できない領域では検索できない。)
  • 湖を点ではなく領域で表現する。(レコードは点としてしか位置情報を持てない。)

文章だけだとピンとこないはずなので、図も用意しました。

まず、以下の図を見てください。黒い点がレコードを表しています。それぞれの操作でレコードがどのように扱われるかを示します。

レコードのみ

以下の図は「指定した四角の中に含まれている座標を持つレコードを検索」したところです。赤い四角が「指定した四角」で、赤い点が検索されたレコードです。

指定した四角の中に含まれている座標を持つレコードを検索

以下の図は「指定した円の中に含まれている座標を持つレコードを検索」したところです。赤い円が「指定した円」で、赤い点が検索されたレコードです。

指定した円の中に含まれている座標を持つレコードを検索

以下の図は「座標間の距離を計算」したところです。赤い点が基準点で、基準点とレコードの座標の間の距離を計算しています。

座標間の距離を計算

以下の図は「ある座標からの距離が近い順にレコードをソート」したところです。赤い点が基準点で、基準点からの距離が近い順にレコードを順番に選んでいます。赤い数字が選ばれた順番です。

ある座標からの距離が近い順にレコードをソート

表現方法

前述の通り、groongaで保持できる位置情報は点だけです。点を格納するカラムは以下のどちらかの型にしなければいけません。

  • TokyoGeoPoint: 日本測地系での座標のときに利用する。
  • WGS84GeoPoint: 世界測地系(WGS 84のWGSはWorld Geodetic Systemの略)での座標のときに利用する。

どちらの型を用いた場合でも、緯度と経度を格納するという点は変わりません。そのため、どちらの型の値も同じ表現方法を用います。サポートしている表現方法は以下のフォーマットの文字列です。

  • "#{緯度}x#{経度}"
  • "#{緯度},#{経度}"

緯度・経度は「ミリ秒」または「度」で表現します。ミリ秒表記はあまりなじみがないかもしれませんが、度表記はGoogle Mapsでも使われている表記なので見たことがあるかもしれません。たとえば、東京駅は緯度が35度40分52.975秒、経度が139度45分57.902秒ですが、これは以下のように表現します。

ミリ秒表記:

  • 35度40分52.975秒 → ((35 * 60 * 60) + (40 * 60) + 52.975) * 1000 → 128452975
  • 139度45分57.902秒 → ((139 * 60 * 60) + (45 * 60) + 57.902) * 1000 → 503157902
  • 座標: "128452975x503157902" または "128452975,503157902"

度表記:

  • 35度40分52.975秒 → 35 + ((40 + (52.975 / 60.0)) / 60.0) → 35.6813819444444
  • 139度45分57.902秒 → 139 + ((45 + (57.902 / 60.0)) / 60.0) → 139.766083888889
  • 座標: "35.6813819444444x139.766083888889"または"35.6813819444444,139.766083888889"(これはGoogle Mapsと同じ表記。つまり、Google Mapsで表示した座標情報をそのまま利用できる。)

以下は使用例です。まず、テーブルとカラムを定義します。

% groonga -n /tmp/geo-point
> table_create Stations TABLE_HASH_KEY ShortText
[[0,1315881737.57395,0.055109867],true]
> column_create Stations location COLUMN_SCALAR WGS84GeoPoint
[[0,1315881759.65377,0.081054688],true]

データを取り込みます。上記の4パターンすべてを取り込んでいます。

> load --table Stations
> [
> ["_key", "location"],
> ["東京駅(ミリ秒 + x表記)", "128452975x503157902"],
> ["東京駅(ミリ秒 + ,表記)", "128452975,503157902"],
> ["東京駅(度 + x表記)", "35.6813819444444x139.766083888889"],
> ["東京駅(度 + ,表記)", "35.6813819444444,139.766083888889"]
> ]
[[0,1315881767.69242,127.240681505],4]

格納されているデータを確認します。groonga内部では緯度・経度をミリ秒として保持しているため、ミリ秒表記で出力されます。groonga内部でミリ秒として保持しているのは浮動小数点数ではなく整数として処理したいからです。

> select Stations --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="4" NHITS="4">
<HIT NO="1">
<FIELD NAME="_id">1</FIELD>
<FIELD NAME="_key">東京駅(ミリ秒 + x表記)</FIELD>
<FIELD NAME="location">128452975x503157902</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_id">2</FIELD>
<FIELD NAME="_key">東京駅(ミリ秒 + ,表記)</FIELD>
<FIELD NAME="location">128452975x503157902</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_id">3</FIELD>
<FIELD NAME="_key">東京駅(度 + x表記)</FIELD>
<FIELD NAME="location">128452974x503157901</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_id">4</FIELD>
<FIELD NAME="_key">東京駅(度 + ,表記)</FIELD>
<FIELD NAME="location">128452974x503157901</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

目視でデータを確認する場合は「--output_type xml」を指定して、XMLとして出力した方が確認しやすいです。

使い方

groongaの位置情報検索機能で利用できる以下のことについてその実現方法を説明します。

  1. 指定した四角の中に含まれている座標を持つレコードを検索する。
  2. 指定した円の中に含まれている座標を持つレコードを検索する。
  3. 座標間の距離を計算する。
  4. ある座標からの距離が近い順にレコードをソートする。

説明にあたって、店舗を検索するアプリケーションを考えます。各店舗がそれぞれ1レコードに対応します。

スキーマ定義

まず、店舗を格納する「Shops」テーブルを定義します。今回は説明用なので、各店舗には必要最小限の情報として店舗名と位置情報のみを格納することとします。

table_create Shops TABLE_HASH_KEY ShortText
column_create Shops location COLUMN_SCALAR WGS84GeoPoint

位置情報で高速に検索できるようにインデックスを張ります。

table_create Locations TABLE_PAT_KEY WGS84GeoPoint
column_create Locations shop COLUMN_INDEX Shops location

インデックス用のテーブル「Locations」はパトリシアトライ(TABLE_PAT_KEY)にします。キーの型はインデックス対象のカラム(Shops.location)と同じ型(WGS84GeoPoint)にすることがポイントです。

以下は実際に実行した結果です。

ddl.grn:

table_create Shops TABLE_HASH_KEY ShortText
column_create Shops location COLUMN_SCALAR WGS84GeoPoint

table_create Locations TABLE_PAT_KEY WGS84GeoPoint
column_create Locations shop COLUMN_INDEX Shops location

データベースの作成:

% rm -rf /tmp/shops
% mkdir -p /tmp/shops/
% groonga -n /tmp/shops/db < ddl.grn
[[0,1315883158.10711,0.05206624],true]
[[0,1315883158.1593,0.067047364],true]
[[0,1315883158.22642,0.056288895],true]
[[0,1315883158.28277,0.11994776],true]
サンプルデータ

たまたまたいやき屋(+α)のデータがあった*2のでそのデータを使います。

shops.grn:

load --table Shops
[
["_key", "location"],
["根津のたいやき", "35.720253,139.762573"],
["たい焼 カタオカ", "35.712521,139.715591"],
["そばたいやき空", "35.683712,139.659088"],
["車", "35.721516,139.706207"],
["広瀬屋", "35.714844,139.685608"],
["さざれ", "35.714653,139.685043"],
["おめで鯛焼き本舗錦糸町東急店", "35.700516,139.817154"],
["尾長屋 錦糸町店", "35.698254,139.81105"],
["たいやき工房白家 阿佐ヶ谷店", "35.705517,139.638611"],
["たいやき本舗 藤家 阿佐ヶ谷店", "35.703938,139.637115"],
["みよし", "35.644539,139.537323"],
["寿々屋 菓子", "35.628922,139.695755"],
["たい焼き / たつみや", "35.665501,139.638657"],
["たい焼き鉄次 大丸東京店", "35.680912,139.76857"],
["吾妻屋", "35.700817,139.647598"],
["ほんま門", "35.722736,139.652573"],
["浪花家", "35.730061,139.796234"],
["代官山たい焼き黒鯛", "35.650345,139.704834"],
["たいやき神田達磨 八重洲店", "35.681461,139.770599"],
["柳屋 たい焼き", "35.685341,139.783981"],
["たい焼き写楽", "35.716969,139.794846"],
["たかね 和菓子", "35.698601,139.560913"],
["たい焼き ちよだ", "35.642601,139.652817"],
["ダ・カーポ", "35.627346,139.727356"],
["松島屋", "35.640556,139.737381"],
["銀座 かずや", "35.673508,139.760895"],
["ふるや古賀音庵 和菓子", "35.680603,139.676071"],
["蜂の家 自由が丘本店", "35.608021,139.668106"],
["薄皮たい焼き あづきちゃん", "35.64151,139.673203"],
["横浜 くりこ庵 浅草店", "35.712013,139.796829"],
["夢ある街のたいやき屋さん戸越銀座店", "35.616199,139.712524"],
["何故屋", "35.609039,139.665833"],
["築地 さのきや", "35.66592,139.770721"],
["しげ田", "35.672626,139.780273"],
["にしみや 甘味処", "35.671825,139.774628"],
["たいやきひいらぎ", "35.647701,139.711517"]
]

データのロード:

% groonga /tmp/shops/db < shops.grn
[[0,1315883204.86313,0.005284274],36]

それでは、サンプルデータが用意できたので実際に使ってみましょう。

指定した四角の中に含まれている座標を持つレコードを検索

明日、あなたは初めて浅草に行くことになりました。初めて行く土地にたいやき屋があるかどうか、気になりますよね。そこで、浅草周辺にあるたいやき屋をgroongaで検索することにしました。

地図を見ると、左上が「35.7185,139.7912」で右下が「35.7065,139.8069」となる四角い範囲の中にたいやき屋があるかどうかを調べれば浅草周辺のたいやき屋を見つけられそうです。

浅草周辺のたいやき屋をgeo_in_rectangle()で検索

groongaにはgeo_in_rectangle(カラム名, 四角い範囲の左上の座標, 四角い範囲の右下の座標)という関数があり、この関数を--filterオプションに指定すると指定した四角い範囲内にあるレコードをインデックスを使って高速に検索することができます。

% groonga /tmp/shops/db
> select Shops --filter 'geo_in_rectangle(location, "35.7185,139.7912", "35.7065,139.8069")' --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="2" NHITS="2">
<HIT NO="1">
<FIELD NAME="_id">30</FIELD>
<FIELD NAME="_key">横浜 くりこ庵 浅草店</FIELD>
<FIELD NAME="location">128563246x503268584</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_id">21</FIELD>
<FIELD NAME="_key">たい焼き写楽</FIELD>
<FIELD NAME="location">128581088x503261445</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

浅草周辺には、くりこ庵と写楽があるんですね。それでは、写楽の方に行くことにしましょう。

補足: 実際はブラウザ内の地図表示エリアに表示している範囲にあるレコードを検索するためにgeo_in_rectangle()を使うことが多いでしょう。なぜなら、ほとんどの地図表示エリアは四角だからです。

指定した円の中に含まれている座標を持つレコードを検索

浅草周辺を検索するために四角を指定するのは少し面倒ですね。それよりも、「浅草駅から500m以内にあるたいやき屋」の方がわかりやすいです。

地図を見ると、浅草駅は「35.7119,139.7983」にあります。

浅草周辺のたいやき屋をgeo_in_circle()で検索

groongaにはgeo_in_circle(カラム名, 円の中心の座標, 円の半径)という関数があり、この関数を--filterオプションに指定すると指定した円の範囲内にあるレコードをインデックスを使って高速に検索することができます。

% groonga /tmp/shops/db
> select Shops --filter 'geo_in_circle(location, "35.7119,139.7983", 500)' --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="1" NHITS="1">
<HIT NO="1">
<FIELD NAME="_id">30</FIELD>
<FIELD NAME="_key">横浜 くりこ庵 浅草店</FIELD>
<FIELD NAME="location">128563246x503268584</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

> 

浅草駅の近くには、くりこ庵しかないんですね。それでは、くりこ庵に行くことにしましょう。

座標間の距離を計算

東京駅から2km以内にあるたいやき屋を検索し、それぞれのたいやき屋までの距離も取得しましょう。距離を取得するには_scoreカラムとgeo_distance(カラム名, 基準点の座標)関数を使います。

_scoreカラムは擬似カラムの一種で、検索結果レコードに自動的に追加されているカラムです。通常は検索のヒットスコアを入れるのですが、それ以外の値でも任意の値を入れることができるので、東京駅からの距離を入れることにします。

--scorerオプションを指定することにより_scoreカラムに任意の値を設定できます。ここでgeo_distance()を使い、東京駅からの距離を_scoreカラムに入れます。

実行例(読みやすくするため改行が入っていますが実際は改行を入れてはいけません):

> select Shops
    --filter 'geo_in_circle(location, "35.68138194,139.766083888889", 2000)'
    --scorer '_score = geo_distance(location, "35.68138194,139.766083888889")'
    --output_columns '_key,_score,*'
    --sortby _score
    --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="7" NHITS="7">
<HIT NO="1">
<FIELD NAME="_key">たい焼き鉄次 大丸東京店</FIELD>
<FIELD NAME="_score">230</FIELD>
<FIELD NAME="location">128451283x503166852</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_key">たいやき神田達磨 八重洲店</FIELD>
<FIELD NAME="_score">407</FIELD>
<FIELD NAME="location">128453259x503174156</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_key">銀座 かずや</FIELD>
<FIELD NAME="_score">990</FIELD>
<FIELD NAME="location">128424628x503139222</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_key">にしみや 甘味処</FIELD>
<FIELD NAME="_score">1310</FIELD>
<FIELD NAME="location">128418570x503188660</FIELD>
</HIT>
<HIT NO="5">
<FIELD NAME="_key">しげ田</FIELD>
<FIELD NAME="_score">1606</FIELD>
<FIELD NAME="location">128421453x503208982</FIELD>
</HIT>
<HIT NO="6">
<FIELD NAME="_key">柳屋 たい焼き</FIELD>
<FIELD NAME="_score">1671</FIELD>
<FIELD NAME="location">128467227x503222331</FIELD>
</HIT>
<HIT NO="7">
<FIELD NAME="_key">築地 さのきや</FIELD>
<FIELD NAME="_score">1765</FIELD>
<FIELD NAME="location">128397312x503174595</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

鉄次が一番近いですね。

ある座標からの距離が近い順にレコードをソート

前の例では_scoreに入れた東京駅からの距離でソートしていましたが、このときはインデックスを使いません。インデックスを使ってソートする場合は--sortbyにgeo_distance()関数を指定します。ただし、--sortby内では緯度・経度の区切りに「,」は使えないことに注意してください。--sortbyでgeo_distance()を使うときは、「"#{緯度},#{経度}"」ではなく「"#{緯度}x#{経度}"」というように緯度・経度の区切りには「x」を使ってください。

  • ×: --filter 'geo_distance(location, "35.68138194,139.766083888889")'
  • ○: --filter 'geo_distance(location, "35.68138194x139.766083888889")'

実行例(読みやすくするため改行が入っていますが実際は改行を入れてはいけません):

> select Shops
    --filter 'geo_in_circle(location, "35.68138194,139.766083888889", 2000)'
    --sortby 'geo_distance(location, "35.68138194x139.766083888889")'
    --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="7" NHITS="7">
<HIT NO="1">
<FIELD NAME="_key">たい焼き鉄次 大丸東京店</FIELD>
<FIELD NAME="location">128451283x503166852</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_key">たいやき神田達磨 八重洲店</FIELD>
<FIELD NAME="location">128453259x503174156</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_key">銀座 かずや</FIELD>
<FIELD NAME="location">128424628x503139222</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_key">にしみや 甘味処</FIELD>
<FIELD NAME="location">128418570x503188660</FIELD>
</HIT>
<HIT NO="5">
<FIELD NAME="_key">しげ田</FIELD>
<FIELD NAME="location">128421453x503208982</FIELD>
</HIT>
<HIT NO="6">
<FIELD NAME="_key">柳屋 たい焼き</FIELD>
<FIELD NAME="location">128467227x503222331</FIELD>
</HIT>
<HIT NO="7">
<FIELD NAME="_key">築地 さのきや</FIELD>
<FIELD NAME="location">128397312x503174595</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

インデックスを使わない場合と同じ結果になっていますね。

データ構造

ここまではユーザ向けの説明でしたが、ここからは実装の説明になります。

groongaでは位置情報を高速に検索するために、GeoHashと同じ考え方で緯度経度をエンコードしてパトリシアトライに格納しています。同じ考え方というのは、緯度と経度の情報を交互に含んだバイト列としてエンコードするという点です。

例えば、東京駅はミリ秒表記では「128452975x503157902」になります。groongaは内部では緯度・経度をミリ秒としてデータを持っていて、それぞれ32bit整数として保持しています。東京駅のデータは2進数で表すと以下のようになります。

何番目のビットか 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
緯度(128452975) 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0 0
経度(503157902) 0 0 0 1 1 1 0 1 1 1 1 1 1 1 0 1
何番目のビットか 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
緯度(128452975) 0 0 0 0 1 0 0 1 0 1 1 0 1 1 1 1
経度(503157902) 1 0 0 1 0 1 0 0 1 0 0 0 1 1 1 0

この緯度・経度データをエンコードして1つのビット列にし、それをパトリシアトライのキーとします。このとき、緯度のビットと経度のビットを交互に使います。

何番目のビットか 0 1 2 3 4 5 6 7 8 9 10 11 12 13 ...
エンコードされた緯度・経度データ 0 0 0 0 0 0 0 1 0 1 1 1 1 0 ...
緯度の何番目のビットか 0 1 2 3 4 5 6 ...
緯度(128452975) 0 0 0 0 0 1 1 ...
経度の何番目のビットか 0 1 2 3 4 5 6 ...
経度(503157902) 0 0 0 1 1 1 0 ...

こうすることにより、先頭の方から2ビットずつデータを読んでいけば緯度情報と経度情報を両方読み込むことができるデータ構造になります。さらに、先頭の方がより粗い位置情報(より広い範囲を表す情報)となっているので、先頭からデータを読み込むことにより徐々に範囲を絞り込んでいけます。

検索方法

では、このデータ構造を使ってどのように効率よく検索するかを説明します。

以下の図は先頭の2ビットだけを読んだ状態の図です。

先頭2ビットだけを読んだ状態

先頭の2ビットが「00」の場合は右上の赤い範囲を表します。つまり、赤い範囲にあるレコードを検索したい場合は先頭の2ビットが「00」のレコードを検索すればよいことになります。

さらに2ビット読んで先頭の4ビットまで使うことにしたのが以下の図です。

先頭4ビットまで読んだ状態

先頭の4ビットが「0000」であれば、右上の範囲の中のさらに左下の赤い範囲にあることがわかります。

このようにして先頭から2ビット単位でデータを読み込むことによりレコードを絞り込むことができます。先頭ビットでレコードを絞り込んでいく部分はパトリシアトライの前方一致検索機能を使います。そのため、位置情報のインデックス用のテーブルはパトリシアトライである必要があります。

geo_in_rectangle()を使った検索

geo_in_rectangle()がどのように検索しているかを説明します。

geo_in_rectangle()は、まず,指定された四角よりも少しだけ大きい範囲を選びます。例えば、黒塗りの四角が指定された場合は赤い縁になっている2つの範囲を選びます。このとき、できるだけ小さい範囲を選ぶようにがんばります。

geo_in_rectangle()で使う範囲

次に、範囲の中にあるレコードを取り出し、指定された四角の中に本当にレコードが含まれているかを確認します。範囲を少し大きめにとっているため、このチェックをしないと、指定された四角に入っていない(けど近くにある)レコードも検索結果に含めてしまう可能性があるためです。

geo_in_circle()を使った検索

geo_in_circle()も、形が四角ではなく円であるというだけでやっていることはgeo_in_rectangle()とほとんど同じです。違うのはどうやって検索対象とする範囲を選ぶかという部分だけです。

まず、指定された円よりも少しだけ大きい範囲を選びます。例なので、実際の処理よりも大雑把に説明します。以下の図のように黒塗りの円が指定された場合は赤い縁になっている9つの範囲を選びます*3。このとき、できるだけ小さい範囲を選ぶようにがんばります。

geo_in_circle()で使う範囲

次に、範囲の中にあるレコードを取り出し、指定された円の中に本当にレコードが含まれているかを確認します。これはgeo_in_rectangle()でもやっている処理と同じです。

まとめ

groongaのドキュメントの位置情報検索についての情報が不足しているため、現状に合わせた内容をまとめました。ここにまとめた内容は後でgroongaのドキュメントに反映させる予定です。

*1  このあたりに興味のある人はOpen Geospatial Consortiumのサイトもみるとよいでしょう。

*2  元々はgroonga本体のテスト用に用意したデータです。

*3  実際はもっと細かく範囲を分割して、検索範囲をもっと小さくします

つづき: 2011-12-26
タグ: groonga
2011-09-13

おすすめzsh設定

他の人がzshを使っているのを見ていると、「もっと便利に使えるのに」と、もやっとしたり、「え、その便利な機能ってなに?」と、発見があったりします。だれかに「この設定をすると便利ですよ」と話しやすくするために、今のzshのおすすめ設定をここに記しておきます。

もし、Emacsも使っている場合はおすすめEmacs設定もどうぞ。

ディレクトリ構成

長年漬け込んできたEmacsの設定がそこそこの量になっているので、以下のようなディレクトリ構成にして分類しています。主に、zsh標準機能の設定と追加パッケージの設定を分けるために後しています。

~
├── .zshrc                    # シェルを起動する毎に読み込まれる。
│                               # ~/.zsh.d/zshrcを読み込んで
│                               # 標準機能の追加設定を行う。
├── .zshenv                   # ログイン時に一度だけ読み込まれる。
│                               # ~/.zsh.d/zshenvを読み込んで
│                               # 追加設定を行う。
└── .zsh.d                    # zsh関連のファイル置き場。
       ├── config             # 標準機能以外の設定を置くディレクトリ。
       │    └── packages.zsh # 追加パッケージの設定をするファイル。
       ├── zshrc              # おすすめ~/.zshrc設定。
       ├── zshenv             # おすすめ~/.zshenv設定。
       ├── package.zsh        # パッケージ管理システム。
       └── packages           # パッケージをインストールするディレクトリ。

おすすめ設定の使い方

このうち、~/.zsh.d/以下にあるzshrczshenvは一般的なおすすめ設定となっていて、GitHubに置いてあります。

GitHubにあるおすすめ設定は以下のようにして使えます。(以下のコマンドをそのまま実行すると既存の~/.zshrc~/.zshenvを上書きするので注意してください。)

% git clone https://github.com/clear-code/zsh.d.git ~/.zsh.d
% echo "source ~/.zsh.d/zshrc" > ~/.zshrc
% echo "source ~/.zsh.d/zshenv" > ~/.zshenv

これらのおすすめ設定は~/.zshrc~/.zshenvの先頭 で読み込んで、その後でカスタマイズすることを想定しています。 例えばこんな感じです。

~/.zshrc:

### ~/.zshrc

# おすすめ設定を読み込む。
source ~/.zsh.d/zshrc

# これより↓に自分用の設定を書く。
#   setopt ...
#   alias ...
# など

~/.zshenv:

### ~/.zshenv

# おすすめ設定を読み込む。
source ~/.zsh.d/zshenv

# これより↓に自分用の設定を書く。
#   export EMAIL="..."
# など

~/.zshrc~/.zshenvの使い分け

~/.zshrc~/.zshenvは以下のように使い分けます。

  • 対話的に使うときだけ必要なものは~/.zshrc
  • 常に使うものは~/.zshenv

対話的以外にどういう場面で使うかというと、例えば、Emacs上からM-!でコマンドを実行するときです。このときは~/.zshenvのみが読み込まれて~/.zshrcは読み込まれません。

~/.zshenvのおすすめ設定

パスやEmacsなどからも使うページャやgrepなどのコマンドの設定、環境変数の設定などをします。設定の説明はコメントを見てください。

パスの設定

(N-/)は便利なので覚えておいて損はありません。

## 重複したパスを登録しない。
typeset -U path
## (N-/): 存在しないディレクトリは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            /: ディレクトリのみ残す。
path=(# システム用
      /bin(N-/)
      # 自分用
      $HOME/local/bin(N-/)
      # Debian GNU/Linux用
      /var/lib/gems/*/bin(N-/)
      # MacPorts用
      /opt/local/bin(N-/)
      # Solaris用
      /opt/csw/bin(N-/)
      /usr/sfw/bin(N-/)
      # Cygwin用
      /cygdrive/c/meadow/bin(N-/)
      # システム用
      /usr/local/bin(N-/)
      /usr/bin(N-/)
      /usr/games(N-/))
sudo時のパスの設定

sudoを使うときはsbinもパスに入れたいです。ただ、ここで設定したからといって実際にsudoで使われるわけではありません。後で~/.zshrcで設定するsudo時の補完用に使います。そのため、~/.zshenvではなく~/.zshrcで設定したほうがよい気もしますが、パスなので~/.zshenvに入れました。

typeset -Tは便利なので覚えておいて損はないと思います。

## -x: export SUDO_PATHも一緒に行う。
## -T: SUDO_PATHとsudo_pathを連動する。
typeset -xT SUDO_PATH sudo_path
## 重複したパスを登録しない。
typeset -U sudo_path
## (N-/): 存在しないディレクトリは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            /: ディレクトリのみ残す。
sudo_path=({,/usr/pkg,/usr/local,/usr}/sbin(N-/))
man時のパスの設定

(ほとんどありませんが)Emacs上でmanを読むこともあるためman用のパスも~/.zshenvで設定します。

## 重複したパスを登録しない。
typeset -U manpath
## (N-/) 存在しないディレクトリは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            /: ディレクトリのみ残す。
manpath=(# 自分用
         $HOME/local/share/man(N-/)
         # MacPorts用
         /opt/local/share/man(N-/)
         # Solaris用
         /opt/csw/share/man(N-/)
         /usr/sfw/share/man(N-/)
         # システム用
         /usr/local/share/man(N-/)
         /usr/share/man(N-/))
Rubyの設定

Rubyのライブラリを開発するときは./lib$LOAD_PATHに入っていると便利なので、その設定をしています。

## ライブラリのロードパス
### -x: export RUBYLIBも一緒に行う。
### -T: RUBYLIBとruby_pathを連動する。
typeset -xT RUBYLIB ruby_path
### 重複したパスを登録しない。
typeset -U ruby_path
### パスを設定
ruby_path=(# カレントディレクトリのライブラリを優先する
           ./lib)
Pythonの設定

Pythonのライブラリを開発するときは./libsys.pathに入っていると便利なので、その設定をしています。

## ライブラリのロードパス
### -x: export PYTHONPATHも一緒に行う。
### -T: PYTHONPATHとpython_pathを連動する。
typeset -xT PYTHONPATH pyhon_path
### 重複したパスを登録しない。
typeset -U python_path
### パスを設定。
python_path=(# カレントディレクトリのライブラリを優先する
             ./lib)
pkg-configの設定

pkg-configをサポートしたライブラリを使った開発するときの設定です。

## .pcのロードパス
### -x: export PKG_CONFIG_PATHも一緒に行う。
### -T: PKG_CONFIG_PATHとpkg_config_pathを連動する。
typeset -xT PKG_CONFIG_PATH pkg_config_path
### 重複したパスを登録しない。
typeset -U pkg_config_path
### パスを設定。
### (N-/) 存在しないディレクトリは登録しない。
###    パス(...): ...という条件にマッチするパスのみ残す。
###            N: NULL_GLOBオプションを設定。
###               globがマッチしなかったり存在しないパスを無視する。
###            -: シンボリックリンク先のパスを評価。
###            /: ディレクトリのみ残す。
pkg_config_path=(# 自分用
                 $HOME/local/lib/pkgconfig(N-/)
                 # MacPorts用
                 /opt/local/lib/pkgconfig(N-/))
ページャの設定

エンコーディングを変換できることが便利なのでlvを優先して使います。

if type lv > /dev/null 2>&1; then
    ## lvを優先する。
    export PAGER="lv"
else
    ## lvがなかったらlessを使う。
    export PAGER="less"
fi
lvの設定

単に「lv」としただけでもいい感じに動くようにします。

if [ "$PAGER" = "lv" ]; then
    ## -c: ANSIエスケープシーケンスの色付けなどを有効にする。
    ## -l: 1行が長くと折り返されていても1行として扱う。
    ##     (コピーしたときに余計な改行を入れない。)
    export LV="-c -l"
else
    ## lvがなくてもlvでページャーを起動する。
    alias lv="$PAGER"
fi
grepの設定

grepはGNU grepを使い、便利な機能は単に「grep」としただけで使えるようにします。一番便利なのは--color=autoです。

## GNU grepがあったら優先して使う。
if type ggrep > /dev/null 2>&1; then
    alias grep=ggrep
fi
## デフォルトオプションの設定
export GREP_OPTIONS
### バイナリファイルにはマッチさせない。
GREP_OPTIONS="--binary-files=without-match"
### grep対象としてディレクトリを指定したらディレクトリ内を再帰的にgrepする。
GREP_OPTIONS="--directories=recurse $GREP_OPTIONS"
### 拡張子が.tmpのファイルは無視する。
GREP_OPTIONS="--exclude=\*.tmp $GREP_OPTIONS"
## 管理用ディレクトリを無視する。
if grep --help | grep -q -- --exclude-dir; then
    GREP_OPTIONS="--exclude-dir=.svn $GREP_OPTIONS"
    GREP_OPTIONS="--exclude-dir=.git $GREP_OPTIONS"
    GREP_OPTIONS="--exclude-dir=.deps $GREP_OPTIONS"
    GREP_OPTIONS="--exclude-dir=.libs $GREP_OPTIONS"
fi
### 可能なら色を付ける。
if grep --help | grep -q -- --color; then
    GREP_OPTIONS="--color=auto $GREP_OPTIONS"
fi
エディタの設定

visudoなど、エディタが必要になったときはvimを使うようにします。

## vimを使う。
export EDITOR=vim
## vimがなくてもvimでviを起動する。
if ! type vim > /dev/null 2>&1; then
    alias vim=vi
fi
メールアドレスの設定

~/.zsh.d/emailまたは~/emailにメールアドレスを書いておくと、そのファイルの中身を環境変数EMAILに設定します。メールアドレスはそれぞれ違うためこのように設定するようにしました。

## ~/.zsh.d/email → ~/.emailの順に探して最初に見つかったファイルから読み込む。
## (N-.): 存在しないファイルは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            .: 通常のファイルのみ残す。
email_files=(~/.zsh.d/email(N-.)
             ~/.email(N-.))
for email_file in ${email_files}; do
    export EMAIL=$(cat "$email_file")
    break
done

~/.zshrcのおすすめ設定

補完やaliasなど対話的に使ったときに便利な機能の設定をします。こちらも設定の説明はコメントを見てください。

キーバインド

環境変数EDITORにvimと設定しているとviキーバインドになってしまうので、Emacsキーバインドを使うように明示的に指定しています。

## Emacsキーバインドを使う。
bindkey -e
ディレクトリ移動

cdの機能を拡張します。chpwd_functionsは覚えておいて損はないと思います。直接chpwdを定義するよりも追加の設定が簡単になります。

## ディレクトリ名だけでcdする。
setopt auto_cd
## cdで移動してもpushdと同じようにディレクトリスタックに追加する。
setopt auto_pushd
## カレントディレクトリ中に指定されたディレクトリが見つからなかった場合に
## 移動先を検索するリスト。
cdpath=(~)
## ディレクトリが変わったらディレクトリスタックを表示。
chpwd_functions=($chpwd_functions dirs)
ヒストリ

ヒストリはとてもよく使うため基本的に過去のコマンドラインは消えないようにします。

## ヒストリを保存するファイル
HISTFILE=~/.zsh_history
## メモリ上のヒストリ数。
## 大きな数を指定してすべてのヒストリを保存するようにしている。
HISTSIZE=10000000
## 保存するヒストリ数
SAVEHIST=$HISTSIZE
## ヒストリファイルにコマンドラインだけではなく実行時刻と実行時間も保存する。
setopt extended_history
## 同じコマンドラインを連続で実行した場合はヒストリに登録しない。
setopt hist_ignore_dups
## スペースで始まるコマンドラインはヒストリに追加しない。
setopt hist_ignore_space
## すぐにヒストリファイルに追記する。
setopt inc_append_history
## zshプロセス間でヒストリを共有する。
setopt share_history
## C-sでのヒストリ検索が潰されてしまうため、出力停止・開始用にC-s/C-qを使わない。
setopt no_flow_control
プロンプト

以下のような2段プロンプトにします。1段目にはできるだけ現在情報を多めに表示して、後から実行履歴をさかのぼって見たときに有意義な情報が残るようにします。プロンプト内にコマンドの実行ステータスを入れるようにしているのでprint_exit_statusオプションは設定していません。

-(user@debian)-(0)-<2011/09/01 00:54>--------------------[/home/user]-
-[84](1)%                                                         [~]

プロンプトで多くの機能を使えるようにします。

## PROMPT内で変数展開・コマンド置換・算術演算を実行する。
setopt prompt_subst
## PROMPT内で「%」文字から始まる置換機能を有効にする。
setopt prompt_percent
## コピペしやすいようにコマンド実行後は右プロンプトを消す。
setopt transient_rprompt

使っていませんが、256色用の色を生成する機能です。

## 256色生成用便利関数
### red: 0-5
### green: 0-5
### blue: 0-5
color256()
{
    local red=$1; shift
    local green=$2; shift
    local blue=$3; shift

    echo -n $[$red * 36 + $green * 6 + $blue + 16]
}

fg256()
{
    echo -n $'\e[38;5;'$(color256 "$@")"m"
}

bg256()
{
    echo -n $'\e[48;5;'$(color256 "$@")"m"
}

プロンプトにたくさんの情報を盛り込もうと頑張っています。「%」が多くてとても読みづらいですね。

## プロンプトの作成
### ↓のようにする。
###   -(user@debian)-(0)-<2011/09/01 00:54>--------------------[/home/user]-
###   -[84](0)%                                                         [~]

## バージョン管理システムの情報も表示する
autoload -Uz vcs_info
zstyle ':vcs_info:*' formats \
    '(%{%F{white}%K{green}%}%s%{%f%k%})-[%{%F{white}%K{blue}%}%b%{%f%k%}]'
zstyle ':vcs_info:*' actionformats \
    '(%{%F{white}%K{green}%}%s%{%f%k%})-[%{%F{white}%K{blue}%}%b%{%f%k%}|%{%F{white}%K{red}%}%a%{%f%k%}]'

### プロンプトバーの左側
###   %{%B%}...%{%b%}: 「...」を太字にする。
###   %{%F{cyan}%}...%{%f%}: 「...」をシアン色の文字にする。
###   %n: ユーザ名
###   %m: ホスト名(完全なホスト名ではなくて短いホスト名)
###   %{%B%F{white}%(?.%K{green}.%K{red})%}%?%{%f%k%b%}:
###                           最後に実行したコマンドが正常終了していれば
###                           太字で白文字で緑背景にして異常終了していれば
###                           太字で白文字で赤背景にする。
###   %{%F{white}%}: 白文字にする。
###     %(x.true-text.false-text): xが真のときはtrue-textになり
###                                偽のときはfalse-textになる。
###       ?: 最後に実行したコマンドの終了ステータスが0のときに真になる。
###       %K{green}: 緑景色にする。
###       %K{red}: 赤景色を赤にする。
###   %?: 最後に実行したコマンドの終了ステータス
###   %{%k%}: 背景色を元に戻す。
###   %{%f%}: 文字の色を元に戻す。
###   %{%b%}: 太字を元に戻す。
###   %D{%Y/%m/%d %H:%M}: 日付。「年/月/日 時:分」というフォーマット。
prompt_bar_left_self="(%{%B%}%n%{%b%}%{%F{cyan}%}@%{%f%}%{%B%}%m%{%b%})"
prompt_bar_left_status="(%{%B%F{white}%(?.%K{green}.%K{red})%}%?%{%k%f%b%})"
prompt_bar_left_date="<%{%B%}%D{%Y/%m/%d %H:%M}%{%b%}>"
prompt_bar_left="-${prompt_bar_left_self}-${prompt_bar_left_status}-${prompt_bar_left_date}-"
### プロンプトバーの右側
###   %{%B%K{magenta}%F{white}%}...%{%f%k%b%}:
###       「...」を太字のマジェンタ背景の白文字にする。
###   %d: カレントディレクトリのフルパス(省略しない)
prompt_bar_right="-[%{%B%K{magenta}%F{white}%}%d%{%f%k%b%}]-"

### 2行目左にでるプロンプト。
###   %h: ヒストリ数。
###   %(1j,(%j),): 実行中のジョブ数が1つ以上ある場合だけ「(%j)」を表示。
###     %j: 実行中のジョブ数。
###   %{%B%}...%{%b%}: 「...」を太字にする。
###   %#: 一般ユーザなら「%」、rootユーザなら「#」になる。
prompt_left="-[%h]%(1j,(%j),)%{%B%}%#%{%b%} "

## プロンプトフォーマットを展開した後の文字数を返す。
## 日本語未対応。
count_prompt_characters()
{
    # print:
    #   -P: プロンプトフォーマットを展開する。
    #   -n: 改行をつけない。
    # sed:
    #   -e $'s/\e\[[0-9;]*m//g': ANSIエスケープシーケンスを削除。
    # sed:
    #   -e 's/ //g': *BSDやMac OS Xのwcは数字の前に空白を出力するので削除する。
    print -n -P -- "$1" | sed -e $'s/\e\[[0-9;]*m//g' | wc -m | sed -e 's/ //g'
}

## プロンプトを更新する。
update_prompt()
{
    # プロンプトバーの左側の文字数を数える。
    # 左側では最後に実行したコマンドの終了ステータスを使って
    # いるのでこれは一番最初に実行しなければいけない。そうし
    # ないと、最後に実行したコマンドの終了ステータスが消えて
    # しまう。
    local bar_left_length=$(count_prompt_characters "$prompt_bar_left")
    # プロンプトバーに使える残り文字を計算する。
    # $COLUMNSにはターミナルの横幅が入っている。
    local bar_rest_length=$[COLUMNS - bar_left_length]

    local bar_left="$prompt_bar_left"
    # パスに展開される「%d」を削除。
    local bar_right_without_path="${prompt_bar_right:s/%d//}"
    # 「%d」を抜いた文字数を計算する。
    local bar_right_without_path_length=$(count_prompt_characters "$bar_right_without_path")
    # パスの最大長を計算する。
    #   $[...]: 「...」を算術演算した結果で展開する。
    local max_path_length=$[bar_rest_length - bar_right_without_path_length]
    # パスに展開される「%d」に最大文字数制限をつける。
    #   %d -> %(C,%${max_path_length}<...<%d%<<,)
    #     %(x,true-text,false-text):
    #         xが真のときはtrue-textになり偽のときはfalse-textになる。
    #         ここでは、「%N<...<%d%<<」の効果をこの範囲だけに限定させる
    #         ために用いているだけなので、xは必ず真になる条件を指定している。
    #       C: 現在の絶対パスが/以下にあると真。なので必ず真になる。
    #       %${max_path_length}<...<%d%<<:
    #          「%d」が「${max_path_length}」カラムより長かったら、
    #          長い分を削除して「...」にする。最終的に「...」も含めて
    #          「${max_path_length}」カラムより長くなることはない。
    bar_right=${prompt_bar_right:s/%d/%(C,%${max_path_length}<...<%d%<<,)/}
    # 「${bar_rest_length}」文字分の「-」を作っている。
    # どうせ後で切り詰めるので十分に長い文字列を作っているだけ。
    # 文字数はざっくり。
    local separator="${(l:${bar_rest_length}::-:)}"
    # プロンプトバー全体を「${bar_rest_length}」カラム分にする。
    #   %${bar_rest_length}<<...%<<:
    #     「...」を最大で「${bar_rest_length}」カラムにする。
    bar_right="%${bar_rest_length}<<${separator}${bar_right}%<<"

    # プロンプトバーと左プロンプトを設定
    #   "${bar_left}${bar_right}": プロンプトバー
    #   $'\n': 改行
    #   "${prompt_left}": 2行目左のプロンプト
    PROMPT="${bar_left}${bar_right}"$'\n'"${prompt_left}"
    # 右プロンプト
    #   %{%B%F{white}%K{green}}...%{%k%f%b%}:
    #       「...」を太字で緑背景の白文字にする。
    #   %~: カレントディレクトリのフルパス(可能なら「~」で省略する)
    RPROMPT="[%{%B%F{white}%K{magenta}%}%~%{%k%f%b%}]"

    # バージョン管理システムの情報を取得する。
    LANG=C vcs_info >&/dev/null
    # バージョン管理システムの情報があったら右プロンプトに表示する。
    if [ -n "$vcs_info_msg_0_" ]; then
        RPROMPT="${vcs_info_msg_0_}-${RPROMPT}"
    fi
}

## コマンド実行前に呼び出されるフック。
precmd_functions=($precmd_functions update_prompt)
補完

zshのとても大事な機能です。

## 初期化
autoload -U compinit
compinit

補完候補をグループ化するとぐっと見やすくなります。

## 補完方法毎にグループ化する。
### 補完方法の表示方法
###   %B...%b: 「...」を太字にする。
###   %d: 補完方法のラベル
zstyle ':completion:*' format '%B%d%b'
zstyle ':completion:*' group-name ''

補完候補が多いときや日本語のファイル名のときはメニューから選択できると便利です。selectの他にtrueも設定できるのですが、それを設定するとすぐに補完された状態になり使い勝手が悪いため設定していません。

## 補完侯補をメニューから選択する。
### select=2: 補完候補を一覧から選択する。
###           ただし、補完候補が2つ以上なければすぐに補完する。
zstyle ':completion:*:default' menu select=2

パスを補完しているときにディレクトリやシンボリックリンクがパッと見てわかるので便利です。

## 補完候補に色を付ける。
### "": 空文字列はデフォルト値を使うという意味。
zstyle ':completion:*:default' list-colors ""

大文字を入力しなくても大文字の補完候補が出てきて便利です。また、Emacsのpartial-completion-modeのようにも補完できて、public_htmlをp_で補完できて便利です。

## 補完候補がなければより曖昧に候補を探す。
### m:{a-z}={A-Z}: 小文字を大文字に変えたものでも補完する。
### r:|[._-]=*: 「.」「_」「-」の前にワイルドカード「*」があるものとして補完する。
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z} r:|[._-]=*'

多めに補完方法を使っています。

## 補完方法の設定。指定した順番に実行する。
### _oldlist 前回の補完結果を再利用する。
### _complete: 補完する。
### _match: globを展開しないで候補の一覧から補完する。
### _history: ヒストリのコマンドも補完候補とする。
### _ignored: 補完候補にださないと指定したものも補完候補とする。
### _approximate: 似ている補完候補も補完候補とする。
### _prefix: カーソル以降を無視してカーソル位置までで補完する。
zstyle ':completion:*' completer \
    _oldlist _complete _match _history _ignored _approximate _prefix

細々と便利な設定です。

## 補完候補をキャッシュする。
zstyle ':completion:*' use-cache yes
## 詳細な情報を使う。
zstyle ':completion:*' verbose yes
## sudo時にはsudo用のパスも使う。
zstyle ':completion:sudo:*' environ PATH="$SUDO_PATH:$PATH"

## カーソル位置で補完する。
setopt complete_in_word
## globを展開しないで候補の一覧から補完する。
setopt glob_complete
## 補完時にヒストリを自動的に展開する。
setopt hist_expand
## 補完候補がないときなどにビープ音を鳴らさない。
setopt no_beep
## 辞書順ではなく数字順に並べる。
setopt numeric_glob_sort
展開

configureを使うときに便利です。

## --prefix=~/localというように「=」の後でも
## 「~」や「=コマンド」などのファイル名展開を行う。
setopt magic_equal_subst

あまり必要な機会はありませんが、設定しています。

## 拡張globを有効にする。
## glob中で「(#...)」という書式で指定する。
setopt extended_glob
## globでパスを生成したときに、パスがディレクトリだったら最後に「/」をつける。
setopt mark_dirs
ジョブ

別の端末からgdbでアタッチするときにプロセスIDがわかると便利です。

## jobsでプロセスIDも出力する。
setopt long_list_jobs
実行時間

明示的にtimeを使わずに済んで便利です。

## 実行したプロセスの消費時間が3秒以上かかったら
## 自動的に消費時間の統計情報を表示する。
REPORTTIME=3
ログイン・ログアウト

クラックされたときにすぐに気づけ(ることがあり)ます。

## 全てのユーザのログイン・ログアウトを監視する。
watch="all"
## ログイン時にはすぐに表示する。
log

## ^Dでログアウトしないようにする。
setopt ignore_eof
単語

C-wで単語単位で削除するときにパスの1コンポーネントだけ削除できて便利です。M-f/M-bで単語移動をする人も便利でしょう。

## 「/」も単語区切りとみなす。
WORDCHARS=${WORDCHARS:s,/,,}
alias

LGが特に便利です。

## ページャーを使いやすくする。
### grep -r def *.rb L -> grep -r def *.rb |& lv
alias -g L="|& $PAGER"
## grepを使いやすくする。
alias -g G='| grep'
## 後はおまけ。
alias -g H='| head'
alias -g T='| tail'
alias -g S='| sed'

rrをよく使います。

## 完全に削除。
alias rr="command rm -rf"
## ファイル操作を確認する。
alias rm="rm -i"
alias cp="cp -i"
alias mv="mv -i"

poをよく使います。pdauto_pushdを設定しているため使いません。

## pushd/popdのショートカット。
alias pd="pushd"
alias po="popd"

素のlsを使うことはほとんどなく、いつもlaを使っています。

## lsとpsの設定
### ls: できるだけGNU lsを使う。
### ps: 自分関連のプロセスのみ表示。
case $(uname) in
    *BSD|Darwin)
        if [ -x "$(which gnuls)" ]; then
            alias ls="gnuls"
            alias la="ls -lhAF --color=auto"
        else
            alias la="ls -lhAFG"
        fi
        alias ps="ps -fU$(whoami)"
        ;;
    SunOS)
        if [ -x "`which gls`" ]; then
            alias ls="gls"
            alias la="ls -lhAF --color=auto"
        else
            alias la="ls -lhAF"
        fi
        alias ps="ps -fl -u$(/usr/xpg4/bin/id -un)"
        ;;
    *)
        alias la="ls -lhAF --color=auto"
        alias ps="ps -fU$(whoami) --forest"
        ;;
esac
ウィンドウタイトル

タブを切り替えているときに便利です。

## 実行中のコマンドとユーザ名とホスト名とカレントディレクトリを表示。
update_title() {
    local command_line=
    typeset -a command_line
    command_line=${(z)2}
    local command=
    if [ ${(t)command_line} = "array-local" ]; then
        command="$command_line[1]"
    else
        command="$2"
    fi
    print -n -P "\e]2;"
    echo -n "(${command})"
    print -n -P " %n@%m:%~\a"
}
## X環境上でだけウィンドウタイトルを変える。
if [ -n "$DISPLAY" ]; then
    preexec_functions=($preexec_functions update_title)
fi

おまけ: package.zshの使い方

GitHubにあるおすすめ設定の中にpackage.zshという簡単なパッケージ管理*1システムも入っています。これを使えばGitHub上にあるzshを拡張するパッケージを簡単にインストールすることができます。例えば、auto-fuを使う場合は以下のようにします。auto-fuの設定をするファイルが~/.zsh.d/config/packages.zshなのはauto-fuがzshの標準機能ではなく追加パッケージだからです。このポリシーについてはこのページの先頭にある「ディレクトリ構成」のところを見直してください。

~/.zsh.d/config/packages.zsh:

# -*- sh -*-

# パッケージ管理システムを読み込む。
source ~/.zsh.d/package.zsh

# パッケージがインストールされていなければGitHubからcloneしてくる。
package-install github hchbaw/auto-fu.zsh
# パッケージを読み込む。
source $(package-directory hchbaw/auto-fu.zsh)/auto-fu.zsh
# auto-fuを初期化する。
zle-line-init() {
    auto-fu-init
}
zle -N zle-line-init
zle -N zle-keymap-select auto-fu-zle-keymap-select

# auto-fuをカスタマイズする。
## Enterを押したときは自動補完された部分を利用しない。
afu+cancel-and-accept-line() {
    ((afu_in_p == 1)) && { afu_in_p=0; BUFFER="$buffer_cur" }
    zle afu+accept-line
}
zle -N afu+cancel-and-accept-line
bindkey -M afu "^M" afu+cancel-and-accept-line

~/.zshrc:

### ~/.zshrc

# おすすめ設定を読み込む。
source ~/.zsh.d/zshrc

# 追加パッケージの設定を読み込む。
source ~/.zsh.d/config/packages.zsh

これで次にzshを起動するとauto-fuが使えるようになっています。

まとめ

長年漬け込んできたzshの設定を紹介しました。

ここで紹介した内容はGitHubに置いておいたので、興味がある人は試してみてください。

おすすめEmacs設定もありますので、Emacsも使っているひとはそちらも試してみてください。

*1  「管理」というより「インストール」というか「ダウンロード」。

つづき: 2011-12-26
2011-09-05

xml2po: XMLをgettextを使って国際化

需要がないためか、Web上にあまり情報がないxml2poというツールについて紹介します。

xml2poとは

xml2poはXML内のテキスト情報を他の言語へ翻訳することを支援するツールです。ちなみにPythonで書かれています。

xml2poを使って英語のXML文書を日本語に翻訳する場合の流れは以下のようになります。

  1. 英語のXMLからテキスト部分を抽出する。
  2. テキストを翻訳する。
  3. 元の英語のXMLと翻訳済みのテキストを合わせて日本語のXMLを生成する。

図にすると以下のようになります。

英語のXML文書を日本語に翻訳

翻訳方法

xml2poは翻訳のバックエンドにGNU gettextを採用しています*1。そのため、翻訳作業はGNU gettextを用いて翻訳する場合とほとんど同じようになります。GNU gettextを使ったことがある人にはxgettextの代わりにxml2poを使うといえばピンとくるかもしれません。

コマンドでいうと以下のような流れになります。

% xml2po --mode xhtml --output hello.pot index.html
% msginit --input hello.pot --output ja.po --locale ja
% emacs ja.po
% xml2po --mode xhtml --po-file ja.po --language ja --output index.html.ja index.html

それぞれの作業を簡単な具体例を使ってみてみましょう。

テキストを抽出

まず、翻訳元の英語のXHTMLを用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Hello</title>
  </head>

  <body>
    <h1>Hello</h1>
    <p>This is <em>English</em> XHTML.</p>
  </body>
</html>

このXHTMLからxml2poを使って翻訳対象のテキストを抽出します。

% xml2po --mode xhtml --output hello.pot index.html

中身は以下のようになります。

msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2011-08-24 23:33+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: index.html:5(title) index.html:9(h1)
msgid "Hello"
msgstr ""

#: index.html:10(p)
msgid "This is <em>English</em> XHTML."
msgstr ""

このファイルを元に日本語用の翻訳ファイル(POファイル)を作ります。メールアドレスを聞かれるので入力します。

% msginit --input hello.pot --output ja.po --locale ja
ユーザが翻訳に関するフィードバックをあなたに送ることができるように,
新しいメッセージカタログにはあなたの email アドレスを含めてください.
またこれは, 予期せぬ技術的な問題が発生した場合に管理者があなたに連絡が取れる
ようにするという目的もあります.

Is the following your email address?
  kou@localhost
Please confirm by pressing Return, or enter your email address.
kou@clear-code.com
http://translationproject.org/team/index.html を検索中... 完了.
A translation team for your language (ja) does not exist yet.
If you want to create a new translation team for ja, please visit
  http://www.iro.umontreal.ca/contrib/po/HTML/teams.html
  http://www.iro.umontreal.ca/contrib/po/HTML/leaders.html
  http://www.iro.umontreal.ca/contrib/po/HTML/index.html

ja.po を生成.
%

これで翻訳準備は完了です。

翻訳

次は、ja.poファイルを編集して英語のテキストを翻訳します。msginit "..."のテキストが翻訳対象の英語のテキストです。それに対応する翻訳後の日本語のテキストをmsgstr "..."のところに入力します。

ja.po(変更前):

...
msgid "Hello"
msgstr ""
...
msgid "This is <em>English</em> XHTML."
msgstr ""

ja.po(変更後):

...
msgid "Hello"
msgstr "こんにちは"
...
msgid "This is <em>English</em> XHTML."
msgstr "これは<em>日本語</em>のXHTMLです。"

このPOファイルを便利に編集するためのツールがいくつかあるので、それを利用するとよいでしょう。例えば、Emacsにはpo-mode.elがあります。

国際化

翻訳ができたら、元の英語のXHTMLと翻訳したテキストを使って日本 語のXHTMLを生成します。

% xml2po --mode xhtml --po-file ja.po --language ja --output index.html.ja index.html

生成されたXHTMLは以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>こんにちは</title>
  </head>

  <body>
    <h1>こんにちは</h1>
    <p>これは<em>日本語</em>のXHTMLです。</p>
  </body>
</html>

きちんと翻訳されていますね。

まとめ

XML文書を国際化するためのツールであるxml2poを紹介しました。xml2poを使えば、ドキュメントツールが国際化に対応していなくても、XHTMLを出力してくれさえすればドキュメントを国際化できます。つまり、YARDをドキュメントシステムとして使いながら国際化したドキュメントを生成できるということです。実際にrroongaのリファレンスマニュアルがこの方法を採用しています。

XML文書を国際化したい場合はxml2poを使ってみてはいかがでしょうか。今回はXHTMLに対して使いましたが、DocBook形式のサポートの方が充実しているので、DocBookのファイルを国際化したい場合は有力な選択肢になるでしょう。

*1  もう少し言うと、GNU gettextのファイルフォーマット仕様と管理ツールとPythonで実装されたGNU gettextの仕様に対応したライブラリを用いる。

2011-08-24

tDiaryのRDスタイルにCodeRayを使ったシンタックスハイライト機能を追加

タイトルにキーワードを埋め込んでみました。ククログはtDiaryのRDスタイルで書いています。コードを貼り付けることも多々あるため、シンタックスハイライトもできるようにしてあります。今まではGNU Source-highlightでシンタックスハイライトをしていたのですが、CodeRayでシンタックスハイライトをするように変更しました。

設定方法

~tdiary/clear-code-tdiary/にクリアコードのtDiary関連Subversionリポジトリをチェックアウトしているとします。この状態でtdiary.confに以下のように記述すると、RDスタイルでシンタックスハイライトできるようになります。

1
2
3
clear_code_tdiary_base = "/home/tdiary/clear-code-tdiary"
require "#{clear_code_tdiary_base}/patches/after-load-styles-hook"
require "#{clear_code_tdiary_base}/patches/rd-syntax-highlight"

事前にgemでCodeRayをインストールしておくことを忘れないでください。

使い方

以下のようにverbatimブロックに「# source: ruby(言語名)」というようなヘッダーを入れます。

tdiary.confの設定:(←ここはパラグラフ。)

  # source: ruby (←ここは表示されない。)
  @style = "RD" (←ここがRubyプログラムとしてシンタックスハイライトされる。)

乗り換え理由

GNU Source-highlightからCodeRayに乗り換えたのは以下の理由からです。

  • Rubyのライブラリとして使える。
  • Rabbitで使ってみていい感じだった。

まとめ

ほとんど需要がないはずですが、tDiaryのRDスタイルにシンタックスハイライト機能を追加したので紹介しました。

タグ: Ruby
2011-08-11

Being Geek

6月にギークナード向けの本が出版されました。どちらも技術力はすごいけど人間関係が難しいような人を指す言葉として使われていますね*1

Being Geek ―ギークであり続けるためのキャリア戦略
Michael Lopp/夏目 大
オライリージャパン
¥ 2,415

ギーク向けということになっていますが、ギークの人よりも人間関係をうまくやれるギークではない人が読んだ方がよい内容になっています。話題の多くはどうすればうまい具合に仕事ができるかということです。職場でうまく立ち振る舞うにはこうしたらよいというのが著者の経験を元に書かれているのですが、元々人間関係が苦手な人には実践することは難しそうです。

経験を元に職場での立ち振る舞い方を話してくれる人が周りにいないなら参考になるでしょう。あるいは、この職場でこのままやっていっていいのか、と誰にも相談できずに悶々としている人にもなにかヒントがあるかもしれません。

唯一「人間関係が難しい」という部分に向き合っているのが『23章 「ナード」ハンドブック』のところです。ここには、ギーク・ナードはいかに問題があって、それに対応するにはどうしたらよいかということが書かれています。本書ではナードの"パートナー向けに書"いたとありますが、パートナーに読ませるのではなく、自分で読んでみるのはいかがでしょうか。自分が本当にギーク・ナードかの判断に使えます。読んでみて、書かれていることの多くが当たっていて、頭を抱えてパートナーに謝りたくなったのなら、あなたは本当にギーク・ナードなんでしょう。

1章が数ページの読み物、という構成になっており、どこからでも好きなところから読むことができます。別の章の話が前提にあるところには「何章を参照」というような記述があるので、関連がある章があるところから読み始めてしまっても大丈夫です。気軽に読むことができるので、目次を見て気になるものがあったら手にとってみてはいかがでしょうか。

*1  自称ギークの人は人間関係云々は関係なく「自分は技術力がある」という意味合いで使っているように見えます。ただ、明確な定義があるわけではなさそうなので、「なんとなく」や「雰囲気」で使うのがちょうどよい気がします。

2011-08-03

伝えることを伝えること

しばらく人前で話す予定がないので、講演者が本当に話したかったスピーチを残しておきます。

実は、日本Ruby会議2011で話したこと札幌Ruby会議02で話したこと札幌Ruby会議03で話しかけたことの続きでした。話の流れも札幌Ruby会議02と同じ流れにしました。一番伝えたいことは話しの真ん中に持っていき、その後に実例を伝えるという流れです。

札幌Ruby会議のころは「伝えること」について考えていました。

私がプログラミングを始めたのは大学のころ*1で、世間で活躍しているプログラマーよりだいぶ遅いです。でも、プログラミングが好きでたくさんプログラムを書いてきました。今では、プログラミングをはじめて10年くらい経ち、だいぶ上手くなってきました。でも、このままだとよくないなぁと思うようになりました。

私は独学でプログラミングを学んできました。本を読んだりコードを書いたり、他の人のよいところを盗んだり。でも、他の人も同じようにやる必要はないなぁと思うようになりました。自分がよいとわかったことを厳選して伝えられて、その分、他の人がもっと別のことをできるなら、そっちの方がいいなぁと思ったのです*2

そのため、札幌では自分がプログラミングで大事だと思っていることを伝えようとしました。

それから、普段でもいろいろ伝えてみました*3。 そしたら、伝えるだけでも足りないと思うようになりました。「魚をあげるんじゃなくて、魚の捕り方を教えなよ」では足りないのです。これでは、魚を捕れる人は増えるかもしれませんが、「魚の捕り方を教えなよ」という人は増えないのではないかと思うようになりました。「伝えること」だけではなく「伝えることを伝えること」をしなければいけないのではないか。

「伝えることを伝えること」は日本Ruby会議2011では落とした話題ですが、拾ってくれた人がいて嬉しかったです*4。いつかリベンジしたい話題です。

まだ、「伝えること」も伝えきれていないので、「伝えることを伝えること」はもう少し先の話になると思いますが、実現できたらいいなぁと思っています。まずは、クリアコード内で「伝えること」をまとめて、ここで公開しようとしているところです*5。公開したものが誰かの役にたつといいなぁ。

日本Ruby会議2011のことはもう書いたので、ここに書くことは技術的な話に戻そう*6と思っていました。でも、日本Ruby会議2011関連のWeb日記を読んでいたら、「書いておかないと」と思ったので、書いてしまいました*7

*1  今、ざっと読みなおしてみたら札幌で話したこととか練馬で話したことと同じことを話していました。変わっていないですね。

*2  ソフトウェアに関してはプログラミングをはじめたころからそんな風に思っていた気がします。自分がよいと思ったソフトウェアがあったら他の人にも「これを使いなよ」と渡せるとよいなぁと思います。だから、自由に使えるソフトウェアが好きなんだと思います。

*3  そして、いろいろうまくいきませんでした。

*4  参考文献を教えてもらいました。手元に用意したので、そっちも少し調べてみようと思います。

*5  自由に利用できるようにする予定です。

*6  RDocとYARDとI18Nの話とか。

*7  ここでは「思いました。」という風には書かないようにしているのですが、Ruby会議関連の話題のときだけは勝手に「特別に使ってよい」ことにしています。

つづき: 2011-12-26
タグ: Ruby
2011-07-25

日本Ruby会議2011: テスティングフレームワークの作り方

日本Ruby会議2011で「テスティングフレームワークの作り方」について話してきました。前に人前で話したのが2月のフクオカRuby大賞だったので半年くらい人前に出ていなかったのですね。

テスティングフレームワークの作り方

発表内容のこととRuby会議のことについて書いておきます。

発表内容

数年前なら自分がよいと思っている技術的なことを「これかっこいいでしょ!」みたいな感じで話したかったのですが、最近は自分がよいと思っている開発スタイルをまだそれを知らない人に「こういうスタイルもあるんだよー」と伝えたくなってきました*1。そのため、今回の発表内容は「自分が開発しているときに無意識のうちに考えていること」になりました。

発表内容についてざっくりまとめるとこんな感じになります。

  • ライブラリやツールを作るときは、頭の中で考えていることをそのまま書けるような使い方にするといいよ。
  • そのためには「だれが」「なんのために」「頭の中で考える」かを意識しよう。

キーワードは「判断基準」にしました。

「判断基準」はここ数年で意識することになったキーワードで、今まで無意識でやっていたことを改めて考えるきっかけになっていました。今のところ、1回の話で伝えられる程には自分の中で整理できているわけではなく、何度も繰り返し伝えていくことでしか伝えきれないと感じています。今回は1回だけの機会になるので伝えきれる気がしなかったのですが、せめてきっかけくらいになればいいなぁと思い、伝えようとしてみました。

本当は一緒に開発をしながら伝えていきたいことです。現状だとクリアコードに就職するしか方法がないのですが、別の方法でもなにか機会があるといいなぁと思っています。

Ruby会議のこと

Ruby会議ではセッションを聞かずにロビーなどをぶらぶらしながら誰かと立ち話をするのが好きでした。ふだんは直接会えない人と初めて会ったり久しぶりに会ったりできるのがRuby会議でした。会ったことがない人同士を引き合わせるのも好きでした。

今回のRuby会議ではクリアコード関係者が4人も壇上で話していました。クリアコードができた頃はこんなことになるとは思ってもいませんでした。とても嬉しいことです。

Ruby会議じゃない場所でもまた会えるといいなぁと思います。

*1  でも、まだ、「これかっこいいでしょ!」という内容を話すときの方が楽しいです。

タグ: Ruby
2011-07-19

RSpecとtest-unit 2での抽象化したテストの書き方の違い

日本Ruby会議2011の3日目の「テスティングフレームワークの作り方」の準備をしていますが、30分だと詰め込み過ぎになってしまうので、話さないことを事前に書いておきます。それは、テストを抽象化するためのAPIの違いです。

RSpecとtest-unit 2でのAPIの違いというと、class UserTest < Test::Unit::TestCasedescribe Userassertshouldの違いの方が目に付きますが、抽象化するためのAPIにもツールの特徴が出ています。抽象化するためのAPIはテストの量が増えてくると必要になる大事な機能です。ここでは、その中でも「テストを共有するAPI」について考えます。

まず、ツールの考え方について確認し、その後、それぞれのツールでどのようなAPIになっているかをみます。

ツールの考え方

まず、それぞれのツールの考え方について確認しましょう。

RSpecの考え方

RSpecでの書き方を見る前に、RSpecがどういうことを実現するためのツールとして開発されているかを確認しましょう。

BDD is an approach to software development that combines Test-Driven Development, Domain Driven Design, and Acceptance Test-Driven Planning. RSpec helps you do the TDD part of that equation, focusing on the documentation and design aspects of TDD.

[RSpec Documentationより引用]

ざっくり訳すと、

BDDはテスト駆動開発とドメイン駆動設計と受け入れテスト駆動計画づくりとATDPを合わせたソフトウェア開発の方法で、RSpecはそのうちのテスト駆動開発の部分だけをお手伝いしますよ。もう少し言うと、テスト駆動開発の特徴であるドキュメントと設計の部分を特に重視しています。

となります*1

ドキュメントと設計(とRSpecというツール名)を合わせて考えると、仕様をテストとして実行できる形にしながら開発を進めたいのではないかという解釈ができます。そうすると、仕様とテストを一緒に書きやすいAPIを目指しているはずです。

test-unit 2の考え方

test-unit 2はxUnit系のテスティングフレームワークです。Rubyで書かれたコードのテストをRubyで書けることを重視しています*2。そのため、Rubyとしてテストを書きやすいAPIを目指しています。

書き方

それでは、このような考え方を持つツールはどのようなAPIを提供するかを見てみましょう。

RSpecでの書き方

RSpecでテストを共有する場合はit_behaves_likeを使います。以下は、shared examples - Example Groups - RSpec Coreにあるコードです。「別途記述した動作通りに動くこと」と読めるAPIになっていますね。期待した動作を取り込むのではなく、参照しているように読めるところがポイントです。

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
require "set"

shared_examples "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      collection.size.should eq(3)
    end
  end

  describe "#include?" do
    context "with an an item that is in the collection" do
      it "returns true" do
        collection.include?(7).should be_true
      end
    end

    context "with an an item that is not in the collection" do
      it "returns false" do
        collection.include?(9).should be_false
      end
    end
  end
end

describe Array do
  it_behaves_like "a collection"
end

describe Set do
  it_behaves_like "a collection"
end
test-unit 2での書き方

test-unit 2でテストを共有する場合は共有したいテストを書いたModuleincludeします。こちらは「テストの実装を共有する」と読めるAPIになっていますね。RubyではModuleは実装を共有する手段として提供されているため、それをそのまま「テストを共有」するために使っていることがポイントです。

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
require "set"

gem "test-unit"
require "test/unit"

module CollectionTests
  def collection
    @collection ||= collection_class.new([7, 2, 4])
  end

  def test_initilize
    assert_equal(3, collection.size)
  end

  def test_include_true
    assert_true(collection.include?(7))
  end

  def test_include_false
    assert_false(collection.include?(9))
  end
end

class ArrayTest < Test::Unit::TestCase
  include CollectionTests

  def collection_class
    Array
  end
end

class SetTest < Test::Unit::TestCase
  include CollectionTests

  def collection_class
    Set
  end
end

まとめ

テスティングフレームワークの作り方からもれた話題のひとつである「 RSpecとtest-unit 2の考え方の違いとそれが『テストを共有するAPI』にどう現れているか」をみてみました。

考え方としてRSpecとtest-unit 2のどちらがよいかではなく、自分がやろうとしている作業にはどちらが合っているかを考えるべきです。仕様としても使えるテストとRubyとして書けるテストのどちらが必要か・重要かを考えます。

例えば、すでにある仕様書や要望リストを実現するために作業している場合はRSpecの方が作業に合っているかもしれません。仕様っぽいAPIである程度Rubyと切り離して作業することにより、仕様を意識しながら作業を進めることができます。

そうではなく、内部で使うためのもので外部とのインターフェイスとなっていない部分であれば、test-unit 2の方が合っているかもしれません。RubyのそのままのAPIを使ってテストを書くため、仕様としてどうかということよりも、Rubyのプログラムとしてどのように動くのがよいかという部分に集中できます。

*1  テスト駆動開発がドキュメントを重視していたかどうかを覚えていないので、訳し間違いかもしれません。

*2  それとテストを自動化できること。

タグ: Ruby
2011-07-12

本社移転

先月お知らせした通り、本日、本社を移転しました。明日から新オフィスでの業務開始となります。

以前のオフィスよりも2倍以上広くなり、ホワイトボード周りを広々使えるようになりました。

クリアコードの設立日は2006年7月25日なので、今月からちょうど6期目に入りました。フリーソフトウェアをビジネスにして5年間存続でき、そして、新しいオフィスで6期目をスタートできることをとても嬉しく思います。

つづき: 2011-12-26
2011-07-11

Sphinxに言語切り替えリンクを付ける方法

ちゃんと書こうとするといつまでも完成しないような気がしたので、小出しにすることにしました*1

groongaのリファレンスマニュアルは英語版と日本語版を用意しています。英語版ページには同じ内容の日本語版ページへのリンク、日本語版ページには同じ内容の英語版ページのリンクを用意しています。各ページの上のところにある国旗画像がそのリンクです。以下のように国旗画像で言語を表現しています。

他言語版のページへのリンク

これはSphinxのテーマを使って実現しています。そのやり方について説明します。

ディレクトリ構成

以下のようなディレクトリ構成になっているとします。en/html/以下にはsphinx -Dlanguage=en ...で生成したHTMLがあり、ja/html/以下にはsphinx -Dlanguage=ja ...で生成したHTMLがあります。

.
|-- en
|   `-- html
|       | ... HTMLがたくさん
|       `-- index.html
`-- ja
    `-- html
        | ... HTMLがたくさん
        `-- index.html

この場合、ja/html/index.htmlでは../../en/html/index.htmlへリンクを張ればよいことになります。

設定方法

conf.pyとテーマのlayout.htmlを設定します。

まず、conf.pyで以下のように設定し、-Dlanguage=...で指定した言語をテーマ内で参照できるようにします。

conf.py:

html_context = {"language": language}

次に、テーマ内でlanguageの値を参照してリンクを生成します。groongaのようにページの先頭の方にリンクを入れる場合は以下のようにします。

layout.html:

{% extends "default/layout.html" %}
{% block header %}
  <ul>
  {%- if language != "en" %}
    <li><a href="{{ pathto('../../en/html/', 1) }}{{ pathto(pagename, 0, '.') }}">English</a></li>
  {%- endif %}
  {%- if language != "ja" %}
    <li><a href="{{ pathto('../../ja/html/', 1) }}{{ pathto(pagename, 0, '.') }}">日本語</a></li>
  {%- endif %}
  </ul>
  {{ super() }}
{% endblock %}

pathto(pagename, 0, '.')でトップページから現在のパスへの相対パスを取得している所がポイントです。Sphinxのドキュメントにはこのやり方が書いていませんでしたが、Sphinxのソースを読んでみたらできそうだったのでこうしました。

languageを参照して出力内容を変えるという手法は他のものにも応用できそうですね。

まとめ

groongaのリファレンスマニュアルで実現している、他言語ページへのリンクを自動生成する方法を紹介しました。Sphinxで複数言語用のドキュメントを生成する場合に利用してみてください。

*1  まとまっていなくて、後から参照しづらいのではないかという点が心配です。

2011-07-07

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|
タグ:
RubyKaigi2008Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer