PHP/WordPress で国際化対応のコードを書く

PHP/WordPress で国際化対応プログラムを書くことがあったので、得た知識をまとめておきます。

gettext について

自分で gettext を使ったことがなくても .mo という拡張子のつくファイルをどこかでみたことはあると思います。 プログラムの中で


_("Enter your name:")

と書いたものを以下のようなエントリを含むソースファイル (.po) から生成した翻訳ファイル (.mo) を使って実行時に翻訳します。


msgid  "Enter your name:"
msgstr "名前を入力してください"

すると最初の _() は以下に翻訳されます。 PHP では _ は gettext 関数のエイリアスとなっており、この関数により文字列が変換されるのです。


"名前を入力してください"

一般的にプログラムで文字列出力時にならないと確定できない部分には printf のようにプレースホルダーを使用可能な関数を用います。 PHP でも printf や sprintf が用意されているのでプレースホルダーつき文字列を引数としてこれらの関数を呼び出すことができますし、以下のように番号つきプレースホルダーを使って順番を変えるテクニックも国際化対応ではよく使われます。


msgid  "Address: %1$s, %2$s"       (US は番地から)
msgstr "住所: %2$s %1$s"       (日本は市区町村から)

Gnu gettext

翻訳ファイルの作成にあたって、私が主に使っているのは Gnu gettext です。 .mo ファイルを作る場合、とりあえず使用する Gnu gettext パッケージのコマンドは以下の通りです。 作業の流れにそって紹介します。

xgettext
ソースファイルから .pot (テンプレートとして使う .po ファイル)
msginit
.pot ファイルから .po ファイルを生成
msgfmt
.po ファイルから .mo ファイルを生成
msgmerge
ソースファイルを修正したときに、修正後に再生成した .pot ファイルと旧 .po ファイルから新 .po ファイルを生成。

実行時に気をつけるのは xgettext のオプション (これは後述) と msgmerge のファイルの指定間違いぐらいで、あとはすんなり使用できると思います。 各コマンドとも出力ファイルは -o で指定できます。 それぞれのコマンドの詳細はここでは取り上げないので、公式マニュアルを参照してください。 Web 検索すれば、.pot ファイルから日本語翻訳ファイルを作る方法は、その他のツールを使う場合含め多くの事例を見つけることができると思います。

msgfmt のデフォルト動作についてちょっと追記。

  • fuzzy とマークされたエントリは無視されるので、内容を確認し fuzzy を消しておく必要がある
  • msgstr が空 ("") のエントリは無視されるのでわざわざ消す必要なし
  • どの翻訳ファイルが使用されるか

    PHP の gettext の場合、翻訳ファイルの存在するディレクトリは bindtextdomain の呼び出しによって「ドメイン」 (通常はアプリケーション名) に結びつけられ、ドメインとロケールの設定から読み出す翻訳ファイルが決まります。 指定されたドメインでベースとなるディレクトリが確定し、指定されたロケールでその配下のどの翻訳ファイルが読み出されるか決まる、ということです。 bindtextdomain で "./lang" ディレクトリを指定したとき、ロケール ja 用の翻訳ファイルをは以下の場所に置きます。 詳しくは gettext のマニュアルの実行例をご覧ください。

    
    ./lang/ja/LC_MESSAGES/modulename.mo
    

    ドメインの指定は textdomain を呼んでセットしておくか、dgettext のようにドメインつきの gettext 系関数を使用します。

    ロケールの変更

    続いてロケールの設定ですが、PHP ではデフォルトのロケール設定は php.ini の ‘intl.default_locale’ の値が使用されます。 これを変えたいときは、Locale::setDefault を呼び出します。 以下はブラウザの設定によりロケールを変更する実行例です。 ブラウザに設定されている言語を HTTP リクエストヘッダーから読み出すのに Locale::acceptFromHttp という関数が用意されています。

    
    if ( !empty( Locale::acceptFromHttp( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) ) {
      Locale::setDefault( Locale::acceptFromHttp( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) );
    }
    

    ソースファイルを作成する

    国際化を念頭に置くのであれば最初から文字列は英語で gettext 系関数を用いて書くべきです。 何故最初からかというと、やはり後から国際化する方が手間がかかるからです。 量としてもそうですが、質としても後付け国際化はさほど面白くない作業になると思うので、最初からやっておきましょう。

    そして、何故英語かというと、残念ながら日本語から各言語に翻訳してくれる人は英語から翻訳してくれる人に比べてずっと少ないですし、そもそも ASCII 文字でないとツールが対応していないからです。 嘆いても仕方ないので、意味が伝わることを目標に英語を使って書いてみましょう。 意味さえ通じれば、きっと誰かがまともな英語翻訳ファイルを作ってくれるはずです!

    PHP の gettext とコンテキスト

    PHP で gettext を使うには必要なモジュールを組み込んでおく必要があります。 Fedora であれば php-intl というパッケージが必要でした。 また、Windows 環境では以下の拡張モジュールが必要でした。

    • php_gettext.dll
    • php_intl.dll

    例えば、英語の「YES/NO」と日本語の「はい/いいえ」が全く同じではないことはみなさんご存知だと思います。 それは _("YES") を "はい" と訳す場合と (まれにですが) "いいえ" と訳す場合があるということですが、このような場合 _("YES", "context") のように 2つめの引数を使ってどの訳語とするかの判断に用います。 この 2つめの引数をコンテキスト情報と呼び、多くのプログラミング言語ではコンテキスト対応した i18n 関数ライブラリが用意されています。

    しかし残念なことに PHP 標準の gettext 系関数はではコンテキストつきで翻訳を行う関数が用意されていません。 従って、これをやろうとするとMozilla Developer Network に紹介されているように以下のような自作関数を用いなければなりません。 (この Mozilla Developer Network のページは一読をお勧めします)

    
    function pgettext($context, $msgid) {
        $contextString = "{$context}\004{$msgid}";
        $translation = _($contextString);
        if ($translation == $contextString)  return $msgid;
        else  return $translation;
    }
    
    function ___($message, $context ="") {
        if($context != "") {
            return pgettext($context, $message);
        } else {
            return _($message);
        }
    }
    

    これで ___("YES", "context") という呼び出しができるようになります。 ここで問題となるのは Gnu の xgettext でソースファイルから .pot を作成するときに、デフォルトではこのような独自関数を扱ってくれないということです。 従って、先の関数を使うためには以下のようなオプションをつける必要があります。

    
    --keyword=___:1 --keyword=___:1,2c
    

    これは ___() 関数を抽出対象とし、2つ以上引数を持つ時は、1番目の引数が変換対象文字列、「c」の付いている 2番目めの引数がコンテキストであることを示しています。 –keyword オプションの詳細はxgettext のマニュアルの「Language specific options」の項にあります。 各プログラミング言語ごとの対応関数もそこに一覧として出ています。

    ところで、1点ハマったのですが、理由はよくわかりませんが以下のように書いた部分が xgettext でエラーとなってしまいました。

    
    <noscript><?php echo _("Please turn on JavaScript"); ?></noscript>
    

    以下のようにコードを直すとエラーを回避できました。 どのような条件でエラーになるのか細かく見ていませんが、このようなことで回避できる場合もあるということです。

    
    (php の別の場所で)
    $msg_js_on = _("Please turn on JavaScript");
    (html の部分で)
    <noscript><?php echo $msg_js_on; ?></noscript>
    

    WordPress の gettext

    PHP 標準関数では足りないので、WordPress では以下のように多くの独自に用意された gettext 系関数が用意されています。 また、ロケールの扱いも独自に実装されており、翻訳ファイルを読み込むディレクトリも PHP の gettext と異なります。

    __(), _e(), _n(), _x(), _ex(), _nx(), esc_attr__(), esc_attr_e(), esc_attr_x(), esc_html__(), esc_html_e(), esc_html_x(), _n_noop(), _nx_noop()

    こうして見ると関数の数は多いのですが、以下のように分類できます。

    • e 付きは echo する
    • n 付きは単数形/複数形の違いを扱う
    • x 付きはコンテキストあり
    • esc_attr 付きは attribute として使うためにエスケープする
    • esc_html 付きは HTML テキスト用としてエスケープする
    • noop 付きは翻訳用配列を用意するための関数

    _n_noop()/_nx_noop() についてはちょっとわかりづらいので補足します。 といっても _n_noop() の公式マニュアルの例が全てなのでこれを説明します。

    
    $messages = array(
    	'post' => _n_noop('%s post', '%s posts'),
    	'page' => _n_noop('%s pages', '%s pages')
    );
    ...
    $message = $messages[$type];
    $usable_text = sprintf( translate_nooped_plural( $message, $count, $domain ), $count );
    

    このコードはポストタイプが投稿か固定ページによって表示するメッセージを変化させる例で、表示用の配列を作るのに _n_noop を使用しています。 同じことは別の書き方でもできるでしょうが、このように書くと実行時に翻訳されるのは実際に使用するポストタイプのメッセージのみになります。

    これだけ関数があると全てを xgettext のオプションとして指定するのは大変なのですが、実行例は見当たりませんでした。 これには理由があって、WordPress に関してはPOT ファイルを作成するためのツールが用意されているので xgettext は不要なのです。 ツールは SVN リポジトリよりチェックアウトして使用します。 以下はプラグイン用の .po (.pot) ファイル生成実行例ですが、my-work-dirmy-plugin-dir は自分の環境に合わせて変えて実行します。 テーマの場合は wp-plugin を wp-theme に変えて実行します。

    $ svn co http://i18n.svn.wordpress.org/tools/trunk/ my-work-dir
    $ cd my-work-dir
    $ php makepot.php wp-plugin my-plugin-dir
    

    WordPress の国際化を行うのであれば、このページを一通り読んでおくべきです。 ディレクトリとドメインの扱いについてもそこにまとめられています。

    まとめ

    PHP/WordPress の gettext についてまとめました。 興味のある方はこれをとっかかりにして i18n 対応のコードを書いてみてください。

    WordPress プラグインを開発して WordPress.Org に登録するまで

    今回 WordPress プラグイン「Thin Out Revisions」を書いて公式リポジトリに登録したわけですが、その辺の流れを書いてみたいと思います。 プラグインを作る動機としては、何かしら個人としてのアウトプットを世間に出したいというのがあったのと、出すならやっぱりグローバルだよね、ってのがありました。 とはいっても私の場合、決して PHP も WordPress も jQuery も熟練者とは言えない状況でした。 それでもプラグインを作ろうとしたきっかけは先のエントリで紹介した「Professional WordPress Plugin Development」で、この内容ががあまりにもよくまとまっていたので、「何か自分でも書いてみたい!」と思ってしまったのです。 そこへ手頃な大きさの課題を見つけることができたので勢いで書きあげてしまいました。

    今回作った Thin Out Revisions は、プログラムのボリュームとしては大したことがありません。 しかし、jQuery も Ajax も使ったし、きちんと Nonce も使って (CSRF対策)、I18N も対応しました。 色々な要素が詰まっていますが、「Professional WordPress Plugin Development」がこれらを包括的に取り上げていたおかげで、さほど苦労せず書き上げることができました。 この本がなければプラグインを書いて公式登録することはなかったでしょうから、著者に感謝です。

    以下時系列に書いてみます。

    9/20 に予てより気になっていた「Professional WordPress Plugin Development」の Kindle 版をダウンロード購入しました。 そして読んでいると自分でもコードを書きたくなり、「そういえばあのリビジョン管理なんとかならないのか」といつも感じていたことを思い出しました。 そこから構想を練り、コードを書き始めました。

    最初のモックアップをローカル git リポジトリにコミットしたのが 9/23 になっています。 その後、作業を続け、9/28 の深夜には終わりが見えてきたので、そろそろ公式リポジトリの登録申請をしておくかという状況になりました。

    9/29 に申請を行い、wordpress.org のチームとメールのやりとりの後 svn リポジトリが用意されたのが 9/30。 そして 10/1、初めて使う subversion のコマンド (私は cvs -> git) でファイルをアップロードし、無事公式サイト上で公開されることとなりました。

    ちょっと焦ったのが svn リポジトリが提供されるよりも先にプラグインパッケージの提出を求められたことです。 「Professional WordPress Plugin Development」での説明順もそうですし、国内開発者のブログ記事でも「まずプラグインの登録申請を行い、後から Subversion をセットアップしてコード登録」という手順になっているので、「リポジトリが用意されるまでに時間がかかりそうだし、早めに申請しておいた方がよいんだろうなあ」と思っていました。 しかし、私の場合は申請後すぐに「プラグインがダウンロード可能な URL を教えるか、ZIP ファイルでプラグインを送ってねー」というメールをもらってしまいました。 ほとんど活動がなかった私のアカウントからの登録申請だったので、チェックが入ったということかも知れません。

    ここで「7日間のうちにレスポンスせよ」と期限を切られていたので急いで最後の調整を行い、初回 ZIP を送付し、その間にマイプラグインページを準備します。 1日も経たず readme.txt がないのを指摘をするメールが届いたので、「readme.txt? 何それ?」という状態からマークダウン記法で readme.txt を仕上げて更新 ZIP を再送付しました。 申請から 2日間でこのやりとりを完結させているのですが、wordpress.org の中の人もレスポンスが良かったのは週末だったお陰かも知れません。

    まあ、日本人としては readme.txt を仕上げるのにも時間がかかるし、普通の会社勤めでは急にまとまった時間が取れないことも多いでしょうから、プラグインを完成させ、(サイトとして完成させていなくても) プラグインホームページ URL や Donate 用 URL を決めてから登録申請を行った方がよいでしょう。 それと readme.txt の内容も本当は時間をかけてプラグイン検索でヒットしやすい様な文章を狙うべきだったと後から気づきました。

    何にせよ、今回の一連のやりとりでいろいろと経験値が貯まったのは確かです。 「手頃な大きさの問題を見つけられたので登録まで行けたんだよな」とも思うので、最初は無理をせず簡単なプラグインを書いて完成させることが重要だと思います。 とは言っても Thin Out Revisions はリビジョン管理好きの人には確実に便利なプラグインに仕上がっており、多くの人に使ってもらえればと思っていますので、興味がある方は是非ダウンロードしてください (宣伝!)。 もし気に入っていただけたら、公式サポートフォーラムで怪しげな英語 (ローマ字?) のやりとりをしましょう!

    WordPress リビジョン管理のための「Thin Out Revisions」

    WordPress を使い始めて最初に気が付くのは投稿/固定ページのリビジョンがどんどん増えていくことです。 基本的にそれらが更新されるたびにリビジョンが増えていく仕様なのですが、これを気にして気軽に保存できなくなるようでは本末転倒です。 そのままだとデータベース使用量が増えるということもあって、リビジョン削除用プラグインは定番カテゴリーを形成しているように見えます。

    しかし、実際には全てのリビジョンを綺麗さっぱり削除してしまうようなプラグインが多く、それではせっかくリビジョン機能が実装されているのにもったいないと思ってしまいます。 世代数で制限をかけるプラグインもありますが、一番最初の版とどう変わったかをみたいときもあると思うので世代数による一律保管制限では機能不足という場合も多いでしょう。 そんな中 Revision Control というプラグインは個別にリビジョンを削除できるので重宝しますが、もう少しスマートにやれればと思ったのでした。

    というような考えで生まれたのがこの Thin Out Revisions プラグインです。 このプラグインを有効化するとリビジョン比較画面 (revision.php) で選択した新旧リビジョンの間にある中間リビジョン全てを一括で削除できます。 使い方は簡単で削除対象を確認してボタンを押すだけです。

    Thin Out Revisions

    また、隣り合った2つのリビジョンを選択した時は、古い方のリビジョンを削除対象とする動作にしました。 これで一番最初に自動保存されてできてしまった目障りな空リビジョンを削除することができます! (私だけ?)

    Thin Out Revisions 2

    公式リポジトリからダウンロードできますので、興味のある方は是非使ってみてください。 ちゃんと日本語対応してます。(日本語のスクリーンショットが必要か…。)


    2012.12.21 追記
    バージョン 1.1 になって余計なリビジョンを作らないための機能が追加されています。→ 関連記事

    2013.2.24 追記
    更にメモをつける機能も追加されました。

    2014.6.14 追記
    日本語公式サイトはこちら。

    WordPress は最初からマルチサイト化しておこう

    WordPress のマルチサイト化について経験に基づきもろもろをまとめておきます。

    はじめに

    「マルチサイト」は 1つの WordPress インストレーションで複数のブログを運用するための機能のことです。 各ブログごとにドメインを分ける「サブドメイン方式」と、同じホスト名でディレクトリを分ける「サブディレクトリ方式」があります。

    インストール完了直後に設定するのがお勧め

    既に運用を始めている WordPress を後からマルチサイト化することもできますが、それなりの設定変更となるので運用を始める前にマルチサイト化しておくのが良いと思います。 一人でブログを書き始めるとしても、将来複数のブログ (例えば、日本語版と英語版とか、仕事用と趣味用とか) を運用したくなるときが来るかも知れないので、とりあえず失敗してもさほど痛くない運用開始前にやっておこうということです。 そもそも最初にやっておいた方が制限も少なくすんなり設定できるはずです。 最初からマルチサイト化しておけば「マルチサイト未対応プラグインを使ってしまい切り替えるときに困る」ということもありません。 マルチサイト機能を有効化しておいて 1つのブログで記事を書き始めれば良いのです。

    運用開始後はサブドメイン方式一択

    運用開始後にマルチサイト化する際の制限として、サブディレクトリ方式はできず、サブドメイン方式でやるしかないというものがあります。 公式情報には以下のようにあります。

    You cannot choose Sub-directory Install (for a path-based network) if your existing WordPress installation has been set up for more than a month, due to issues with existing permalinks.

    一か月経つとパーマネントリンクの何がかわるのかが今一つわからないのですが、基本的にはサブディレクトリ方式でマルチサイト化したいならばインストール直後に設定しなければならないと捉えておくべきでしょう。 その他にもサブドメイン方式を選ぶときは WordPress ディレクトリがドキュメントルートでなければならないと書かれています。 以下のような URL はダメということです。

    http://example.com/wordpress/
    

    あるいは下の様に標準ポートでない場合はそもそもマルチサイト化できません。

    http://example.com:8000/
    

    ハマらないために公式情報はきちんと読んでおきましょう。

    サブドメイン方式の構成

    実際のところ、私もマルチサイト機能を知ったのは運用開始後なので、選択の余地はありませんでした。 というわけでここからはサブドメイン方式の話です。 共用レンタルサーバでは仮想ホスト機能を使いホスト名とディレクトリを対応付けすると思いますが、使用するホスト名は全て同一の WordPress ディレクトリに対応するように設定を行います。 以下のようなイメージです。

    example.com → wordpress ディレクトリ (最初のサイト)
    sub1.example.com → wordpress ディレクトリ
    sub2.example.com → wordpress ディレクトリ
    

    もちろん DNS のレコードもドメインの数だけ登録して同じ Web サーバーを指すようにしておかねばなりません。

    実際にやってみる

    実際の手順は公式情報の通りです。 wp-config.php や .htaccess の修正、blogs.dir の作成といったあたりをこなさなければなりませんが、どのように変更するかは指示されるのであまり迷うこともありません。 あちらこちらのブログにやり方が書かれていますが、中途半端な記事を読むよりは公式情報を読んで作業する方がすんなり行くように思います。 英語も難しくないんで慣れましょう。

    1点注意があるのですが、それは .htaccess の修正です。 これを失敗すると 2番目以降のサイトでアップロードした画像が表示されなかったりします。 私の場合、元の .htaccess は以下の様なものでした。

    <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
    </IfModule>
    

    その .htaccess に以下を追加するよう指示されました。 (少なくとも私にはそう読めました)

    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    
    # uploaded files
    RewriteRule ^files/(.+) wp-includes/ms-files.php?file=$1 [L]
    
    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule ^ - [L]
    RewriteRule . index.php [L]
    

    しかし、実際は後ろに追記しても動かず、正常動作のためには指示された内容に置き替えなければなりません。 このあたり WordPress バージョンや状況によって表示される指示が異なる可能性があるので Apache の Rewrite ルールをある程度把握しておかねばなりません。 元 .htaccess の最初の RewriteCond からの 3行で以下の置き換えが行われます。

    「実体としてのファイルやディレクトリが無い URL は /index.php に書き替え処理を停止する ([L]フラグ)」

    サブドメインサイトにアップロードされたファイルを表示するための書き替えは以下の行なのですが、実体がない URL の置き換えなのでここに来る前に先のルールが適用されると /index.php に変換され処理が終わってしまいます。

    RewriteRule ^files/(.+) wp-includes/ms-files.php?file=$1 [L]
    

    ちなみに追記指示分の最初の RewriteCond からの 3行は以下の置き換えルールです。

    「実体としてファイルかディレクトリが存在する URL は変換せず ("^ -" の部分) 処理を停止する」

    ド・モルガンの法則でしたっけ? 最終行は残り全て index.php に書き替えるので結局元の .htaccess の RewriteCond の部分と同じ結果になるんですよね。 なので、元のルールは必要なく、表示されたもので置き換えれば動きます。 Rewrite ルールは奥が深いです。 私も「^ -」と「. -」の差異とか / があったりなかったり等の細部は気にしないことにしましたが、ある程度は把握していないと動かずに苦労することになるのです。

    ということもあったりなので、インストール直後のマルチサイト化がお勧めなのです。 まあ、この記事を目にする人の多くは既に WordPress を運用しているんだろうとは思いますが…。

    git diff で無視したい行があるとき

    比較時に特定の文字列を含む行を無視したい

    最近 Cisco 機器の設定を git で管理しているのですが、diff を取ると設定をいじっていなくても ntp clock-period の行が表示されてしまいます。 この値は機器が動いている間に自動調整されるものなので変わっていても問題ありません。

    
    -ntp clock-period 36027555
    +ntp clock-period 36027579
    

    何とか無視できないかと調べたところ git-diff には -G オプションというものがあり、正規表現にマッチしたものだけ表示することができるのがわかりました。 しかし、今回は特定の文字列を含む行を除外したいので文字列の否定ルールを表現しなければなりません。 一般的にこのような場合は「否定の先読み」と呼ばれるパターンを使います。 今回のケースだと以下のようになります。

    
    ^(?!ntp clock-period)
    

    しかし残念なことに git-diff は先読み表現に対応していないようで、エラーとなってしまいました。 なので、強引にルールを書いてみます。 以下のようにオプションをつけて git diff を実行すると ntp clock-period の行は消えます。

    
    -G "^([^n]|n[^t]|nt[^p]|ntp[^ ]|ntp [^c])"
    

    ちょっと不格好ですが、これで何とかなります。 ntp 関連で c から始まるコマンドは ntp clock-period のみなので、「ntp c」までを除外すれば十分です。

    diff を外部ツールとして登録してみる

    ところで通常の diff には -I オプションというのがありこちらはマッチしたものを無視するのですんなり書けます。

    -I "^ntp clock-period"

    なのでこのオプション付きで diff を外部ツールとして登録し git difftool コマンドから呼び出すという手もあります。

    
    [diff]
            tool = diffcisco
    [difftool "diffcisco"]
            cmd = \"diff\" -u -I \"^ntp clock-period\" \"$LOCAL\" \"$REMOTE\"
    

    上のように config ファイルに書いて git difftool を実行するのです。 プロンプトがわずらわしいので -y をつけて抑止すると良いです。

    
    $ git difftool -y
    

    ただし、-I オプションは差分となる固まりの全てが正規表現にマッチしている時に出力が抑止されるという動作なので、前後に変更があれば ntp clock-period の行も出力されることになります。 例えば ntp master コマンドが追加された時は ntp clock-period も出力されることになるでしょう。

    git gui で動作させる

    Windows 環境で GUI 操作をすることの多い私としては git gui でこれらを実行させたいところです。 git-gui のオプション画面に「Additional Diff Parameters」(config パラメータとしては gui.diffopts) というのがあるので、ここに以下のように入れてみます。

    
    "-G" "^([^n]|n[^t]|nt[^p]|ntp[^ ]|ntp [^c])"

    クォーテーションの付け方は試行錯誤の結果なのですが、config ファイルには以下の様に「\"」と記録されます。

    
    [gui]
            diffopts = \"-G\" \"^([^n]|n[^t]|nt[^p]|ntp[^ ]|ntp [^c])\"
    

    すると確かに diff では差分無しと処理されているようですが、「コミット予定に入っていない変更」のリストからファイルを消すことはできず、内容を確認しようとしてファイル名をクリックするたびに変更が無い旨のポップアップが表示される動作となり、残念ながら使い勝手は今一つです。

    git diff はそんな状況なので変更有無の確認には git difftool を使うのが良いようです。 「ツール」-「追加」で git difftool -y を呼び出せるようにしておきます。 config には以下のように出力されます。

    
    [guitool "git-diff"]
            cmd = git difftool -y
    

    こうしておくことで git gui 上から git difftool を実行して変更を確認することができるようになります。


    2012.10.4
    正規表現に誤りがあったため、記事を修正しました。

    Windows 環境での git ベストプラクティス

    10年以上前は CVS や Visual Source Safe を使っていたのですが、仕事の内容が変わって長いことバージョン管理システムには縁がない状態でした。 しかし、最近また仕事の内容が変わったのを機に (Subversion をスキップして!) git を導入してみました。 仕事環境は Windows がメインなので git も Windows 上に導入しましたが、使ってわかったのは Windows 環境でも全く問題なく使うことができるということです。 Web 検索すると古い情報が多くて Windows で動かすには苦労話が多いように見えますが、現状全く問題ありません。 といってもやはりノウハウが必要な部分もあるわけで、ここまでの自分の経験をまとめてみます。

    クライアントは Git for Windows で決まり

    ちょっと古い記事だと Cygwin と msysGit のどちらを選択するかという問題を取り上げていると思いますが、現状 msysGit で決まりだと思います。 git を使うだけのために Cygwin を導入するのは大掛かり過ぎます。 まあ、私は必要とすることがあるので Cygwin も導入してはいますが。

    ところで現在の msysGit ですが、一般利用者向けパッケージは「Git for Windows」と名付けられており、「msysGit」は開発者向けパッケージの名前となっています。 ですので、git をバージョン管理に使いたいだけならば msysGit ホームページから「Git for Windows」の方をダウンロードしましょう。 現在のバージョンは Git-1.7.11-preview 版です。 古い記事で msysGit と言っているものは大抵の場合 Git for Windows を指しています。 この記事でも以降 Git for Windows という名前を使います。

    GUI については TortoiseGit が良いよという記事が多いですが、今のところ私は Git for Windows パッケージに含まれている git-gui と gitk で間に合っています。 確かに一部の操作は GUI から実行できず、コマンドライン上で git を使う場面もありますが、私が日々の作業で使う主要な操作のほとんどは GUI 上で出来るので個人的には許容の範囲です。 会社によってはソフトウェアを入れるために IT 管理部門の説得に苦労するようなこともあると思いますが、そのような場合を考えるといろいろなものをあれこれ導入するより「公式の Windows 版 Git です!」といって Git for Windows だけを入れるのが苦労が少ないと思います。

    Windows ファイルサーバーをそのまま利用しよう

    同様にサーバーにソフトウェアを入れるのに困難が伴う会社は多いと思いますが、Git for Windows ならば問題ありません。 Windows サーバー上の共有フォルダをそのままベアリポジトリとして使えるからです。 以前書いたように Git for Windows では Windows の \\servername\folder\sub_folder\repository.git を以下の書式で参照できます。

    //servername/folder/sub_folder/repository.git
    

    サーバー上にリポジトリを置くとアクセス制御が必要となりますが、私の環境ではリポジトリ単位のアクセス制御で十分なので、共有フォルダのアクセス権設定によるアクセス制御で間に合っています。

    エンコードの指定

    Git for Windows 付属の GUI ツールを使っている限り、日本語の扱いも問題ないです。 最近は軟弱になって commit や diff などほとんどの操作を GUI でやっているのが幸いして日本語で困ったことはありません。 とりあえず言語関連について git-gui でやったことは「ファイル」-「編集」で 「ファイル内容のデフォールトエンコーディング」(原文ママ) を utf-8 にすることぐらいです。 (ひょっとしたらデフォルトだったかも)

    また、EUC や SJIS のファイルも扱うことがあると思いますが、.gitattributes を適切に記述すれば git-gui や gitk 上できちんと日本語表示されます。 詳細は gitk の設定と合わせて記事にまとめていますので参照してみてください。

    まあ、コマンドラインツールだとエディタやら less やら bash やらの日本語の扱いの話になってしまうので、どうしてもコマンドラインを使いたければ頑張ってハックしましょう。 私は GUI があるからいいや。

    コマンドラインのお世話になるとき

    そんな軟弱な私ですが、GUI で操作できない処理もあるのでコマンドラインを使うこともあります。 身近なところでは git fetch はできるのに git pull はできなかったりするのでコマンドラインを使います。 「fetch して修正して push して」という流れであれば git-gui の上で済ませられるのですけどね。

    それとファイルの変更を取り消して、前回コミットの状態に戻す操作もそこそこ使います。 GUI だと「コミットから降ろす」+「変更を元に戻す」操作になるので。

    $ git checkout HEAD paths
    

    他にもいくつかコマンドライン上で操作するものはありますが、git-gui には外部ツールを登録することができるので頻繁に使うものは登録してしまうというのも手です。 「git pull origin master」とか登録しておいた方が便利です。 この登録機能は次の Excel 差分比較のところでも使います。

    Word や Excel の管理

    Word にも対応していますが、特に Excel のファイル差分確認には WinMerge がお勧めです。 この WinMerge を差分確認ツールとして使用してみましょう。 なお、ここでは C:\WinMerge の下に WinMergeU.exe が置かれているものとします。 ちなみに WinMergeU.exe は Unicode 版、WinMerge.exe は ANSI 版 (Windows 9x や ME 用) です。

    コマンドラインで使用する

    外部コマンドを使う場合は git diff コマンドを使用するよりも git difftool コマンドを使って比較するのがお勧めです。 git diff で使うためには簡単なシェルスクリプトを用意しなければなりませんが、git difftool であればコンフィグファイルの設定のみで行けます。 以下のようにコンフィグファイル (例えばリポジトリの .git/config) に書いておきます。

    [diff]
            tool = WinMerge
    [difftool "WinMerge"]
            cmd = \"/c/WinMerge/WinMergeU.exe\" -u -wl -wr \"$LOCAL\" \"$REMOTE\"
    

    あとは git diff の代わりにコマンドラインで git difftool と打つだけです。 1点注意ですが、複数のファイルの差分を取るように git diff を呼び出したときでも WinMerge は複数起動せずシリアルに 1つずつ差分を確認していくことになります。 これは一時ファイルを作って差分を取っている仕組みのため仕方がありません。 どうしても複数ファイルの差分を平行して確認したい場合は、Git Bash を複数起動して、それぞれの bash から 1つのファイルだけ指定して git difftool を起動すればよいです。

    設定についてはこちらの記事を参考にさせていただきました。 そちらにはマージ用ツールとしての設定もありますので参照してみてください。 ちなみに私が設定した WinMerge のオプションは MRU リストへの追加抑止と Read-Only のためのものです。 WinMerge のオプションの詳細については公式マニュアルを参照ください。

    git-guiで使用する

    git-gui で使用したいときは「ツール」の「追加」で git difftool を登録するだけです。 起動も「ツール」メニューより行います。

    git-gui

    gitk で使用する

    「編集」「設定」で「外部diffツール」に以下のように入力します。 ちなみに「全体に追加」のチェック/アンチェックで global に設定するか local に設定するかを選ぶことができます。

    c:/WinMerge/WinMergeU.exe
    

    するとコンテキストメニューから「外部diffツール」を起動できるようになります。 先ほどの git difftool の場合と違って WinMerge へうまく動作制御のためのオプションを渡せなかったのですが、比較する 2つのファイル名は渡っているので実用上は問題ないでしょう。

    gitk

    以上で、一通りのツールから WinMerge を差分比較ツールとして起動できるようになりました。 これでかなり便利になると思います。

    改行コードの処理

    改行コード処理については、とりあえずインストール時はデフォルトの「Checkout Windows-style, commit Unix-Style line endings」(core.autocrlf = true) を選んでおけばよいでしょう。 しかし、場合によっては Linux から scp で取ってきたり、ルーターから Expect を使って取ってきたファイルを git リポジトリに突っ込むこともあると思うので、そんなときはリポジトリ単位で core.autocrlf = false に設定して、 CRLF の変換がされないようにした方が管理しやすいかも知れません。

    $ git config --local core.autocrlf false
    

    ファイル名の問題

    日本語ファイル名も問題なく扱うことができます。 ただし、コマンドラインから git を使う可能性のある人は、ファイル名やフォルダ名に日本語を使う場合でも最初の何文字かは ASCII 文字でユニークに区別できる prefix をつけておいた方が良いでしょう。 そうしておけば日本語が入力できなくても bash の補完機能を使って苦労せず生きて行くことができます。

    まとめ

    CVS 等を使ってきた身としては、やはり「分散型」バージョン管理システムの概念を理解するのに時間を要しました。 しかし、実際に使ってみると、職業柄、出先での作業が多いので分散であるところに大いに助けられています。

    ただ、課題もあって Excel のようにマージが難しいものについてはロック機構があればなあと思ってしまいます。 と言っても、そもそも MS Office 系のファイルについて共同作成をする際は章立てごとに担当を決めてファイルを分割して担当するような流れにしないと回らないような気もするので、試行錯誤しながらワークフローを決めていくことになるのでしょう。

    それと「コミットを修正できるってどうよ」という感覚もありますが、その辺は頭を柔らかくして git を使って行きたいと思います。

    最後に git について役に立った参考資料を挙げておきます。 この記事では git の基本的なところはすっとばして Windows での利用時に役立つ話ばかりしているので、使ったことがない人には辛い内容になっているかも知れませんが、もし興味があって「Windows 環境で動くなら手が出せるなあ」と思った人がいたらこれらを参考に是非 git を使い始めてみてください。

    Pro Git
    オンラインならばやはりここだと思います。
    WEB+DB PRESS Vol.50
    雑誌の特集記事ですが、Git について実践的にわかりやすく書かれていて読みやすいです。 今となっては総集編として購入するのが良いでしょう。
    WEB+DB PRESS Vol.69
    メインは GitHub の特集ですが、Git for Windows のインストールについても簡単に説明されています。

    WordPress のプラグインを書こう

    「Kindle で技術系洋書を読もう!」シリーズ (?) の第 2弾です。 WordPress のプラグインを書くのに最適な本を見つけました。 「Professional WordPress Plugin Development」という本です。

    Amazon.com で目次が見れますが、下に各章の概要を書いておきます (章のタイトルそのままもありますが)。 ある程度経験がある方が見ればわかると思いますが、プラグイン開発で必要なものは一通り含まれています。 Kindle 版は約 2千円で、この価格で一通りの情報が得られるので PHP からしてビギナーの私としては大変助かりました。

    WordPress のカスタマイズのためにコードを書こうとしたときに最初に感じたのは情報を見つけにくいことでした。 検索をしても内容が古かったり、特に日本語の情報はテーマのカスタマイズ中心でプログラミングに関しては断片的な情報が多いように感じます。 一応プラグイン開発に関しては公式情報 (日本語英語) がありますが、手を動かし始めると他にも様々な情報が欲しくなってきます。 英語圏まで広げても状況はあまり変わらないようで、序文を読むと著者も同じように感じ、それが執筆の動機となったようです。 確かにこれだけ包括的な情報を一箇所で手に入れるのは難しいと思います。

    プラグイン開発をしなくても、WordPress の動く仕組みを知りたい、あるいはカスタマイズしてみたいというプログラマにはとても有用だと思います。 プログラミング関連書籍の英語は難しくないので興味があれば是非読んでみてください。

    ただし、テーマのカスタマイズ (CSS やマークアップ関連) の情報はありません。 そこは和書がたくさん出ていたと思います。

    1/2章
    プラグインについての基本知識。 推奨コーディングスタイルや開発チェックリストも含まれる。
    3章
    フック (Action と Filter) について。 フック関数としてクラスのメソッド (メンバ関数) を登録する方法。
    4章
    メニューの追加。ウィジェットの作成。メタボックス (投稿等の画面上のボックス区画) 作成と内容の保存。
    5章
    I18N。コードの対応と翻訳ファイルの作り方。
    6章
    セキュリティ。ユーザーのパーミッションやサニタイズ、Nonces (CSRF 対策) 等。
    7章
    設定を保存するための各種 API → *_option()、*_settings_*()、*_transient()、*_user_meta()。 カスタムテーブルへの保存。
    8章
    ユーザの管理。
    9章
    HTTP リクエストを使って Web サービス API を利用する方法。
    10章
    ショートコード。
    11章
    投稿 (post) の拡張。 カスタム投稿タイプやカスタムタクソノミー。
    12章
    JavaScript (jQuery) と Ajax。 フックを使ったサーバーサイド処理。

    13章
    Cron を使ったプログラムのスケジュール実行。
    14章
    URL の Rewrite。
    15章
    マルチサイト。
    16章
    デバッグと最適化。
    17章
    ライセンスの選択。 公式リポジトリへの登録等。
    18章
    各種情報リソース。

    au Wi-Fi SPOT と iPad

    私の iPad は整備済製品として購入した iPad 2 の Wi-Fi モデルで、家庭専用として使用していました。 しかし au の「IS フラット」を契約しているので、Xperia に加えてもう 1台 追加費用なしに au Wi-Fi SPOT を利用することが可能です。 たまたま、時間つぶしが必要になりそうな所用があったので、これを機会に iPad も au Wi-Fi SPOT を利用できるように設定しようと思いたち、au Wi-Fi 接続ツールを iPad 2 にインストールし出かけました。

    時間が無かったため、ろくに調べず、とりあえずアプリをダウンロードして Wi-Fi スポットに行けば良いだろうと考えていたのですが、これが失敗の元でした。 出先の au Wi-Fi SPOT が使えるエリアで au ID を使って初期設定するも、「初期設定ができませんでした。電波状況をご確認いただくか、しばらくしてから再度初期設定を行ってください。」とのエラーメッセージ。 「ID/パスワードを間違ったかも」と思いましたが、これらを再設定するメニューも見当たらずなす術がありません。

    結局、敗因は「au Wi-Fi SPOT の接続を使って初期設定できない」という仕様を理解していなかったためです。 そうなのか。 なので、同じことをする考えている人がいたら、必ず家などのインターネット接続を用いて初期設定をしてから持ち出すようにしましょう。

    SELinux のルールを確認してみよう

    Linux を導入した時に、まず最初にやる作業が「SELinux の無効化」というのはいかがなものかと思っています。 開発環境ならまだしもプロダクション環境で何も考えず無効化しちゃうのは思考停止もいいところ。 しかし、SELinux がとっつきにくいのも確かなわけで、何故とっつきにくいかを自分の経験から考えてみると「現在適用されているルールがよくわからない」ということがあるのではないかと思いました。 世の中に SELinux に関する記事は溢れていますが、そのほとんどは boolean を設定したり、ファイルにタイプを設定したりみたいな枝葉末節を説明しているように思います。 IT系のエンジニアならば、まずは自分の手元の SELinux がどのようなポリシー/ルールで動いているのか知りたくなると思うのですが、それを知ろうとしても、該当する情報はなかなか見つけることができません。

    というわけで今回は SELinux の「現在適用されているルールをどうやって知るか」という点をまとめてみたいと思います。

    前提として:なぜ SELinux を有効化にするのか

    SELinux はファイヤーウォールの置き換えでもアンチウィルスソフトの置き換えでもありません。 なので、それとは別の追加のセキュリティ機構として有効化しておくべきです。 この部分はあちらこちらで説明されているので (例えばちょっと古いですが日経 BP の記事)、詳細は割愛しますが、多層での防御 (defence in depth) ということを考えると、大事な情報を扱うサーバでは SELinux を有効化して運用すべきです。

    SELinux のポリシーの構成

    SELinux のポリシーの肝となる部分は AV (Access Vector) ルールと呼ばれるルールにより構成されています。 一つの AV ルールは以下のように表されますが、これが複数集まってセキュリティポリシーを構成します。

    種類  ソースタイプ(ドメイン)  ターゲットタイプ  :  クラス  パーミッション;
    

    AV ルールには 4種類 (3つの基本ルール+特殊ルール) あります。

    allow ルール
    このルールにより許可されたアクセスはログに記録されません。一番基本となるルールです。
    auditallow ルール
    ログあり許可ルール。allow ルールによって許可されたアクセスはログに記録されませんが、auditallow ルールならば記録されます。
    dontaudit ルール
    ログ出力抑止ルール。通常、ルールにより許可されなかったアクセスは拒否されログに記録されるのですが、dontaudit ルールで定められたアクセスについてはアクセスが拒否されてもログに記録されません。ログ出力を無視してよいアクセスをこのルールで規定します。
    neverallow ルール
    このルールは他の3つとは別格で、ルールのコンパイル時に許さないルールを規定します。allow/auditallow ルールで許可されていても neverallow に引っかかったらコンパイル時にエラーとなります。

    現在のポリシーを把握するには allow ルールと auditallow ルールを中心に確認することになります。

    現在のポリシーを確認してみよう

    現在設定されている AV ルールを表示するには sesearch を使用します。 以下のコマンドで現システムのデフォルトポリシーの allow ルールを表示することができます。

    # sesearch --allow -C
    Found 133955 av rules:
       allow rpm_t httpd_squid_script_exec_t : sock_file { ioctl read write create getattr setattr lock relabelfrom relabelto append unlink link rename };
       allow rpm_script_t xenstored_var_run_t : sock_file { ioctl read write create getattr setattr lock append unlink link rename };
       allow restorecond_t evolution_alarm_exec_t : sock_file { getattr relabelfrom relabelto }
    (以下略)
    

    実行するとこのように万単位のルールが出力されます。 例えば Apache 関連だけを見ようと grep http としても数千行台というレベルで、確かに一つ一つを把握しておけるようなものではありません。 しかし、このようなルールに基づいて SELinux が動作しているということは理解しておくべきです。

    {} は複数の値をまとめるために用いられるので、先の実行例の 1行目は以下のようになります。

    ソースタイプ rpm_t
    ターゲットタイプ httpd_squid_script_exec_t
    クラス sock_file
    パーミッション ioctl, read, write, create, getattr, setattr, lock,
    relabelfrom, relabelto, append, unlink, link, rename

    オブジェクトクラスとパーミッションの一覧はこちらにあります

    また、以下の例の様に ET や DT から始まる行は boolean によって有効化/無効化できるルールを示しています。

     ET allow named_t init_t : process sigchld ; [ init_systemd ]
    

    このように boolean 設定できるルールは行末に [ boolean名 ] が付き、現在有効になっているルールは行頭には ET が、無効になっているルールには DT が付きます。 この例では init_systemd という boolean が設定され有効化されているということになります。 「boolean を semanage でセットしましょう」という記事はあちこちで見かけますが、実際に何が変わるかはこのように sesearch で知ることができるのです。 その boolean 値に関連するルールのみ表示したければ以下のコマンドを使用することができます。

    sesearch --allow -C -b boolean名
    

    また、sesearch で表示されるルールの中には @ が付いた「@ttr0141」のような表記も見られることがありますが、これは attribute です。 attribute はタイプをグループ化するのに用いられ、その attribute が付与されたタイプ一覧は以下のコマンドで確認できます。

    # seinfo -a@ttr0141 -x
    

    ソースファイルではきちんと名前がついていますが、コンパイル後なのでこのような表示となってしまうのでしょう。

    ポリシーはモジュール化されている

    SELinux のフレームワークは NSA が中心となって開発しましたが、ポリシーは民間中心で開発されています。 そして、ポリシーには種類があり、主要なものとしては広く使われている targeted ポリシーとより厳しい strict ポリシーがあります。 身の回りにある CentOS や Fedora ではデフォルトで targeted ポリシーが適用されています。 /etc/selinux/config を見ると確認できます。

    また、ポリシーはモジュール化されています。 targeted ポリシーのモジュール関連ファイルは /etc/selinux/targeted/modules 以下に置かれています。 更にその下の active/modules にある *.pp ファイルがモジュールの実体となっています。

    この .pp ファイルを先の sesearch のパラメータとして渡し、モジュール内のルールを見ることはできるようなのですが、.pp ファイルに依存関係があり、依存するファイル全てを列挙しなければなりません。 列挙する際は base.pp を一番先に記述する必要があります。

    モジュールの依存関係は module_deps で調べることができるので、その結果を sesearch に利用して各モジュールごとの AV ルールを確認することもできそうですが、手元の Fedora ではこのあたりがうまく動かず動作確認できませんでした。

    カスタマイズの仕方

    ポリシーの変更が必要なときは以下の順序で検討します。

    1. boolean の設定で対処できるかの検討
    2. カスタムポリシーの作成

    実際問題としてカスタムポリシーが必要となるのは自作のデーモンを導入するなど既存の枠組みに収めることができない場合のみで、基本的なデーモン類については大抵 boolean の設定で足ります。 それほど様々な boolean 値が用意されています。

    問題はどのような boolean 値が存在しているかをどのように知るかというところですが、いきなり sesearch で細かく見るのではなく、まずマニュアルを探してみるとよいでしょう。 大抵、デーモン名_selinux (httpd_selinux、squid_selinux、…) というマニュアルエントリが用意されているので、例えば Apache については以下のコマンドでマニュアルを確認します。

    $ man httpd_selinux 
    

    まとめと参考記事

    IT 系の仕事をする立場として「SELinux は有効化しておくのが当たり前」という状況になって欲しいですし、「無効化」以外の SELinux の記事が世の中に増えれば良いと願っています。 この記事は SELinux の一部しか説明していないですが、ちょうど今売っているSoftware Design 2012年 9月号で SELinux の仕組みが包括的にわかりやすく説明されているので、興味があればこれをお勧めします。 記事を書くにあたって以下のエントリを参考にさせていただきました。

    Excel で複数のシートを集計する時のテクニック

    ユーザー定義関数で解決!

    先の VBA の話の実践適用例です。

    例えば、「シート1」、「シート2」、…「シート10」までの 10個のワークシートにデータが入っていて、これを集計・操作した「サマリー」シートを作ることを考えてみます。 このような場面はよくあることではないかと思うのですが、ここで困るのがシートの参照の仕方です。 ご存知のように「シート1」のセル A1 は以下のように参照します。

    シート1!A1
    

    このようにワークシートはシート名で参照するので、素直に作業を進めるとワークシートのタブから名前をコピーしてセルに貼り付けるような操作が必要になってしまうわけです。 列は A、B、C、…、行は 1、2、3、… と参照できますが、ワークシートについては名前でしか参照できず、残念ながら「N番目のワークシートを参照する」みたいなワークシート関数も存在しません。

    そこをもう少し表計算ソフトらしく処理するために「ワークシート名を取り出す処理」を考えようということになるのですが、それも簡単ではありません。 例えば、こちらにその方法が丁寧に説明されていますが、何だかなあ、という感じです。

    こんなとき VBA を使って自作関数を作ることができれば、以下のような関数を作って呼べば済みます。

    Function GetWorkSheetNameByIndex(i)
      GetWorkSheetNameByIndex = Worksheets(i).Name
    End Function
    

    1枚目のシートの名前は「=GetWorkSheetNameByIndex(1)」、2枚目は「=GetWorkSheetNameByIndex(2)」というように呼び出して使うことができます。 このようにワークシート関数だけでやろうとすると複雑になってしまう処理でも VBA を使えばシンプルでわかりやすくなる場合があります。

    VBA ユーザー定義関数の再計算

    ここで気をつけねばならないのですが、VBA で作成したユーザー定義関数は F9 を押しても再計算されません。 この問題に対処するために Application.Volatile という関数が用意されていて、これを GetWorkSheetNameByIndex の中で呼ぶとセル修正のたびにこの関数の呼び出しが再計算されるようになります。

    しかし、どこかのセルを修正する度に再計算されることになるので、Application.Volatile を利用するかどうかは慎重に検討すべきです。 先の例ではサマリーシートを作成する段階であればデータ用シート名をそうそう頻繁には変えないでしょうから、わざわざ Application.Volatile を呼ばなくても関数を呼び出しているセルを編集し直して再計算という方法で十分でしょう。

    おまけ

    まあ、「同じ位置のセルを単純集計する」程度の話であれば、ユーザー定義関数を作らなくても 3-D 集計を知っておくだけで済むかも知れません。