@kyanny's blog

My thoughts, my life. Views/opinions are my own.

oh-my-zsh の bash 版を標榜する Bash it を試す

oh-my-zsh という、 zsh 向けの便利な拡張機能をまとめたフレームワークがある。 bash 愛好家としては zsh コミュニティの盛り上がりを悔しいような羨ましいような複雑な気持ちで遠巻きに眺めていたのだけど、 dotfiles.github.com 経由で Bash it という bash 版 oh-my-zsh を標榜するフレームワークを見つけたので試してみた。

https://github.com/revans/bash-it

導入方法は README に書いてあるとおり、 git clone してきて install.sh を実行するだけ。プロンプトでいくつか尋ねられるが、ぼくは Jekyll に関するものは N, プラグインやエイリアスのインストール種別は all を選んだ。実行後は ~/.bash_profile が置き換えられる。オリジナルのファイルは .bash_profile.bak にバックアップされる。 EDITOR, GIT_EDITOR の初期値がちょっと何言ってるかわからない感じなので適時 emacsclient などに変更するのをおすすめする。変更が終わったら . ~/.bash_profile で反映する。

f:id:a666666:20120425043819p:plain

ぼくの場合はこんな見た目になった。 RVM, rbenv などを導入しているとよしなに Ruby のバージョンを表示してくれたり、 Git 管理下のディレクトリにいるときブランチ名を表示するのは当然として、さらに未コミットのファイルがあるかどうかを色とマークで表示してくれたりと細かいところで気が効いている。テーマも豊富で着せ替え的な楽しみもある。テーマの変更は .bash_profile で BASH_IT_THEME を変更すればよい。

Mac OSX Lion (10.7) でプロンプトにゴミが出る問題の解決方法

Mac OSX Lion (10.7) にインストールしたところプロンプトにゴミのような文字列が表示されてしまった。

f:id:a666666:20120425044441p:plain

\e]0;kyanny@kyanny-macbookair.local ~/.bash_it というのがそれで、毎回出るのでたいへんうざったい。これは Bash it の Issues に (重複して) 登録されている既知の問題で、 Issue #108: Prompt command display error · revans/bash-it · GitHub に workaround がある。 Bash のバージョンをあげるか、悪さをしている ~/.bash_it/plugins/enabled/xterm.plugins.bash というプラグインをアンインストールすればよい。 plugins/enabled 以下にあるのはシンボリックリンクでプラグインの実体は plugins/available 以下にある。この方式は Debian/Ubuntu ディストリビューションApache module などで馴染みがある。

まとめ

Bash 版 oh-my-zsh である Bash it を試した。ちょっと fat な気がしなくもないけど、面倒くさがり屋にはちょうどいいかもしれない。逆に、自分で dotfiles を育てゲーするのが趣味だというひとには合わないかも。ぼくはしばらく使ってみようと思う。

ウェブオペレーションを読んだ

発売から遅れること一年、ようやくウェブオペレーションを読んだ。

この本はウェブサイトの運用にまつわるさまざまなトピックを扱ったエッセイ集だが、アプリケーションプログラマにとっても得るものが多い。キャパシティプランニングや監視など、普段の仕事では馴染みの薄いことがらについて初歩的な知識を身につけるのに向いている。もっと抽象的な、姿勢や考え方について述べたエッセイも多くあり、目指すエンジニア像のお手本集としても読み応えがある。

ぼくが特に良いと思ったのは四章、十二章、そして十五章だ。四章は「リーン・スタートアップ」で一躍時の人となったエリック・リースによる継続的デプロイの話だが、思想面で見習うべき点が多かった。個人的に「リーン・スタートアップ」のブームには懐疑的で、大げさにもてはやしすぎじゃないかとちょっと白けているのだけど、エリック・リースその人自身はさすがに経験豊富な起業家であり技術者でもあるだけに含蓄のある内容だった。

十二章は知る人ぞ知る MySQL の世界的なエキスパートであるバロン・シュワルツによるリレーショナルデータベースの扱い方についての話だ。リレーショナルデータベースにまつわる種々の技法について、個人的な経験に基づいてはっきりイエス/ノーと知見を述べている。あの「実践ハイパフォーマンス MySQL」の著者でもあるこの人がそう言うならそうなんだろう、と思わせる説得力がある。例えばシャーディングは複雑すぎるので安易に手を出すな、など、言われてみればもっともなのに、ブログやカンファレンスで頻繁に見聞きするからあたかもやるのが当たり前のような気がしてしまっていることを、ちゃんと考えなおす機会を与えてくれた。

ことさら痛快だったのは「FacebookTwitter などの超有名ウェブ企業がデータベースを使い込んでいる話は注目を集めるが、ほとんどの人にとっては無関係の世界の話なので、惑わされず自分のアプリケーションに必要なことをやれ」という一言だ (原文ママではない) YAPC::Asia 2010 で nekokak さんの省サーバ運用というトークが人気を博したように、みんな頭ではわかっていることだけどついつい有名企業の有名エンジニアがやってる大掛かりなことに目を向けてしまいがちだ。夢を見すぎてぼけた横っ面をひっぱたかれて目が覚めたようなすっきりした気持ちになった。

意外に良かったのが十五章の NoSQL についての話で、 NoSQL として知られるいくつかのソフトウェアを取り上げてそれぞれの特徴を手短に紹介している。パフォーマンスの比較などには触れていないが、単純なベンチマークの数字にばかり目を奪われるとむしろ特徴を見極める邪魔になることもあるため、この構成で良かったと思う。個人的に学ぶところの多い章だった。例えば、 Redis がさまざまなデータ型をサポートしているのは MongoDB とはちょっと違うよな、でも同じ NoSQL なのか、というように、違いがよくわかってなかった点を整理できた。 Riak のように名前を聞いたことがあるだけで特徴もわかっていなかったものについて、ほんのさわりだけとはいえ知識を得ることができたのも収穫だった。

PDF 版ではなく紙の本のほうを買ったが、薄くて軽いので持ち運びやすく、毎日の通勤でも負担にならなかった。三千円を切る価格設定も財布に優しい。 PDF 版ならばさらに安く買える。もうとっくに読んだよ、というひとが大半だろうけど、もし読みそびれている人がいたら、今更だけどおすすめします。

bundle のなかで bundle する

Bundler.with_clean_env と bundle install --gemfile について追記しました

bundle exec した環境下でさらに bundle exec したいことがある。 bundle exec rake resque:work で起動した Resque ワーカーのなかで system("bundle exec rake spec") のような外部コマンドを呼び出すとか。ありますよね。ぼくは最近ありました。そしてハマった (そしてググりづらかった) のでこれ以上犠牲者を増やさないためにブログに書く。

bundler は実行時にいくつかの環境変数を定義するが、この場合問題になるのは BUNDLE_GEMFILE と GEM_HOME だ。 BUNDLE_GEMFILE は bundler が参照する Gemfile のパスで、 GEM_HOME は gem のインストール先となるパスだ。

BUNDLE_GEMFILE は Bundler::Runtime#setup_environment のなかで設定される。ソースを追うと Bundler::SharedHelpers#default_gemfile を経由して Bundler::SharedHelpers#find_gemfile のなかで Gemfile のパスを探している。 find_gemfile はカレントディレクトリを起点として、 Gemfile というファイルが見つかるまで上位ディレクトリにさかのぼっていく。通常 bundle コマンドを実行するときは Gemfile が置いてあるディレクトリに移動しておくだろうから、 BUNDLE_GEMFILE にはカレントディレクトリの Gemfile のパスが設定されることになる。

しかし外部コマンドを呼び出したときは挙動が変わる。まず bundle install した gem に付属する実行ファイル (rake とか) は bundle exec 経由で呼び出せるように bundler 自身がラッパースクリプトで置き換える。このあたりの処理は Bundler::Installer のなかに書いてあり、実行ファイルのラッパースクリプトのひな形が lib/bundler/templates 以下にある。このひな形ファイルのなかで ENV['BUNDLE_GEMFILE'] が未定義だったら設定するようになっており、つまり BUNDLE_GEMFILE が定義済みであればその値をそのまま使う。

Ruby において Kernel.#system や Kernel.#exec などで外部コマンドを呼び出すと子プロセスで指定されたコマンドが実行されるが、親プロセスの環境変数を引き継ぐ。だから、 bundle exec で起動した親プロセスの中から system("bundle exec") という風に起動された子プロセスにおいては BUNDLE_GEMFILE 環境変数が定義済みで、この値は親プロセスの bundle exec を実行したときに参照した Gemfile のパスであるため、子プロセスで実行する bundle コマンドを別ディレクトリで実行しているつもりでもうまく動かない。

一方、 GEM_HOME のほうは gem のインストール先を指定する環境変数で、 bundler においては Bundler#configure_gem_home_and_path を経て Bundler#configure_gem_home というプライベートメソッドのなかで設定される。 configure_gem_home_and_path メソッドは ENV['GEM_HOME'] の有無をチェックして、未定義か disable_shared_gems オプションが指定された場合のみ configure_gem_home が呼び出される。

これも BUNDLE_GEMFILE と同様に、親プロセスで設定された値が子プロセスに引き継がれるため、例えば bundle exec したプロセスの中で、親プロセスの bundle exec を実行したのと別ディレクトリに移動し system("bundle install --deployment") を実行すると、期待に反して依存 gem が vendor/bundle 以下にインストールされない。

そういうディレクトリレイアウトの具体的な例は sorcery で、面白いことに spec/ ディレクトリ以下にバージョン別の Rails アプリケーションを丸ごと持っている。 rake spec すると各 Rails アプリケーションのディレクトリ内で rake spec を実行するのだが、これを Travis CI 上で行うと「bundle のなかで bundle 問題」が起こる。

Travis CI: Building a Ruby Project の Default Test Script や Dependency Management に記載があるように、 Travis CI は依存 gem のインストールとテスト実行に bundler を使う。おそらく bundle install --deployment && bundle exec rake spec のようなコマンドを実行しているのだろう。すると bundle exec rake spec のなかで bundle exec rake spec が実行されることになり、 sorcery の spec は各 Rails アプリケーションの Rails.root にあたるディレクトリ内にある Gemfile に基づいてテストが実行されることを期待しているのに、環境変数 GEM_HOME と BUNDLE_GEMFILE が悪さをしてテストがこける

sorcery にまつわる話は @banyan が教えてくれた。彼が送った Pull Request がマージされ、無事に Travis CI 上での sorcery のテストがパスするようになったようだ。

この問題は親プロセスの環境変数が子プロセスに引き継がれて、子プロセス側の bundler の動作に影響を及ぼすことが原因なので、環境変数をリセットすれば解決する。 Ruby 1.9 では外部コマンドの実行時に環境変数を上書きできるので、

env = {
  'BUNDLE_GEMFILE' => nil,
  'GEM_HOME' => nil,
}
system(env, "bundle install --deployment")

のようにして BUNDLE_GEMFILE と GEM_HOME の値に nil を渡して未定義にしてやれば、子プロセス側の bundler がよしなに環境変数を設定しなおしてくれる。

追記 Wed Apr 25 2012 22:46:41 GMT+0900 (JST)

ブクマコメントで教えてもらったが、 Bundler.with_clean_env というそのものズバリのメソッドが用意されていた。 bundler 1.1 から入った機能のようだ。このブログを書いてる最中に参照した bundle-exec(1) のマニュアルにも ENVIRONMENT MODIFICATIONS の一番最後の Shelling out の項に記載されていた。ソースを読むと BUNDLE_ ではじまる環境変数を削除してブロックを実行してくれるようだ。 clean_system, clean_exec なんてメソッドもある。 GEM_HOME をいじっている箇所は特定できなかった。 bundler/setup も require するので Bundler.setup -> Bundler::Runtime#setup も追ったがわからず。

それとは別に、 bundle install --gemfile オプションの存在もブログを書いてから知った。こちらも bundle-install(1) マニュアルに記載されている。こちらもソースを読むと、 --gemfile で指定したパスにファイルが存在すれば BUNDLE_GEMFILE を上書きしてくれる。こちらも GEM_HOME をどのように扱うのかはわからなかった。

Bundler.with_clean_env は当然 bundler を require していないと使えない。もしくは親プロセス自身が「自分は bundler の中で動いている」と知っている必要があるので、 require 'bundler' した上で常に bundle exec で実行するコードの中であれば、

Bundler.with_clean_env do
  system("bundle install --gemfile /path/to/rails/Gemfile")
  system("bundle exec rake spec")
end

# または
Bundler.clean_system("bundle install --gemfile /path/to/rails/Gemfile")
Bundler.clean_system("bundle exec rake spec")

のように呼び出すのがスマートなのかもしれない (繰り返すが GEM_HOME の扱いがどうなるかは追えていないし試してもいないのでこのコードが動く保証はない)

@banyan 調べでは、 sorcery の Rakefile には require 'bundler/setup' しているコードが "Too slow" とかいうコメントとともにコメントアウトされて残っているそうだ。おそらく bundle のなかで bundle とは別の理由で bundler を require していたがテストの実行に時間がかかり、外しても影響がないのでやめたままになっていてみんな忘れていたが、最近 Travis CI にのせてみたらテストがこけはじめた、けど開発者たちは $ rake spec のように bundle exec を介さず手元でテストを実行していたので何が問題かわからなかった・・・といったところだろうか。

(追記終わり)

思ったよりも長くなった上に途中から bundler のソースコードリーディングになってしまったけど、まとめると、外部コマンドの実行時におかしなことが起きたら環境変数を疑いましょう。それではみなさんすてきな bundle ライフを!