Octomacs を rvm で使う、もしくは Emacs Lisp のデバッグ方法、Emacs Lisp の勉強になったという話

Mar 23, 2013  

個人ブログ、会社ブログとも Octopress を使っている。

Octopress を使ってて思うのは「Emacs から記事を作成(する rake タスクを実行)/編集したい!」

それで octomacs を使ったんだけど、rvm と組み合わせて使った時にハマった。

結果だけを先に述べると、octomacs で rvm を使いたい時は、/etc/ 以下のログインシェルの設定に rvm のパスを追加するようにすれば良い。ベストな解決策では無いだろうが、とりあえず動く。

以下、奮闘記。暇な人向け。

Emacs 拡張の選定

Emacs から Octopress をゴニョニョする Emacs 拡張はパッと探した感じ、2つある。

  • jhelwig/octomacs · GitHub

    • Marmalade に登録されているので、 package.el で設定できる
    • 複数の Octopress プロジェクトに対応
    • rvm/ido に対応
    • 使えるは `octomacs-new-post’ だけ
    • 結構複雑な事をやっている
  • gfreezy/octopress-emacs · GitHub

    • very very very simple
      • ゆえに外から設定とかできない
      • コピーして自分で適当に書き換えて配置する必要がある(rvm化、パスの設定など)
    • 一つの Octopress プロジェクトにだけ対応
    • rbenv のみに対応
    • ひと通りの rake タスクを Emacs から実行できる
      • が、同期実行だから rake gen_deploy とかするとしばらく固まるし rake preview とか rake watch した日には Emacs 動かなくなる

ということで個人ブログだけを管理する時には octopress-emacs を使ってたけど、会社ブログも Octopress になったし、ちょっと前に Emacs 24 化してパッケージ管理を完全に package.el に以降したから、octomacs を使うことにした。

octomacs 設定

package.el を使って rvm と octomacs をインストールしておく。(Marmalade を登録しておく必要あり)

そしたら init.el に

(require 'rvm)
(require 'octomacs)

と書いておく。

プロジェクトの設定は M-x customize-group octomacsOctomacs Workdir Alist を編集するか、init.el か何かに

(custom-set-variables
 '(octomacs-workdir-alist
   (("my-blog"     . "~/my-blog")
    ("kaisha-blog" . "~/kaisha-blog"))))

とか書いておけばおk。alist は `(プロジェクト選択時に出る名前 . ディレクトリパス) という形。

以上が済んだらおもむろに M-x octomacs-new-post を叩く。プロジェクトを選んで、ポスト名を入力する。良い感じ。だが、無残なメッセージと共にコマンドは失敗する:

Unable to create post: /bin/bash: rvm: command not found

無念。

失敗する関数を特定したい

さて、原因を探る。まずやる事は、どの関数で失敗したかを知る事だ。幸い、Emacs にはあるパッケージの呼び出しのプロファイルをとる関数が用意されていたので、それを使ってみる。

M-x elp-instrument-package octomacs

してから、

M-x octomacs-new-post

してプロジェクト名を入力、ポスト名を適当に入れて、失敗する事を確認する。その後、

M-x elp-results

してやると、octomacs パッケージ内での各関数呼び出しのプロファイルが出てくる

octomacs-new-post                    1           5.199352      5.199352
octomacs-new-post-interactive        1           3.722928      3.722928
octomacs-read-project                1           2.010394      2.010394
octomacs-read-post-name              1           1.712456      1.712456
octomacs-rake                        1           0.926343      0.926343
octomacs-rake-with-rvm               1           0.926298      0.926298
octomacs-format-rake-task-with-args  1           2.7e-05       2.7e-05
octomacs-shell-escape-string         1           8e-06         8e-06

今回はプロファイル結果に興味はないので、関数名から怪しい所を探しだす。まー怪しい所は octomacs-rake-with-rvm だろう。関数名上でエンターを押下でその関数定義に飛んでくれた。便利!

shell-command-to-string との出会い

こんな関数定義だった。

(defun octomacs-rake-with-rvm (directory task &optional arguments)
  "Run rake task TASK with specified ARGUMENTS in DIRECTORY using rvm"
  (let* ((default-directory (file-name-as-directory (expand-file-name directory)))
         (rvmrc-path (rvm--rvmrc-locate directory))
         (rvmrc-info (if rvmrc-path (rvm--rvmrc-read-version rvmrc-path) nil))
         (rvm-command (if rvmrc-info
                          (concat "rvm " (mapconcat 'identity rvmrc-info "@") " do ")
                        "")))
    (shell-command-to-string (format "%srake %s" rvm-command (octomacs-format-rake-task-with-args task arguments)))))

エラーを思い出すと

Unable to create post: /bin/bash: rvm: command not found

こんな感じなので、予想としては shell-command-to-string が原因だろうと当たりをつけた。そうすると知りたいのは、この format がどんな文字列を生成しているか。 shell-command-to-string が実行される直前で実行を止めてみてみたい。

Emacs でブレークポイントを貼りたい時は edebug なるものを使うらしい。使い方は、ブレークポイントを貼りたい関数内で C-u C-M-x とする。その後、その関数が評価される時に実行が止まる。知っておくべきコマンドは space でステップ実行、 e で現在のコンテキストでの式の評価、 h でカーソルまで実行、 q でデバッグ終了、位。後は、ステップ実行で式が評価される度にミニバッファに結果を表示していて、実は *Message* バッファにミニバッファの表示がスタックされている事を知っていれば、今回の目的には十分である。

さて、そんなこんなで format が生成している文字列が分かった。こんなの:

"rvm 1.9.3@octopress do rake 'new_post[test]'"

こいつを shell-command-to-string に渡してるらしい。

じゃあちょっとスクラッチバッファで実行してみよう。

(shell-command-to-string "rvm 1.9.3@octopress do rake 'new_post[test]'")
"rake aborted!
No Rakefile found (looking for: rakefile, Rakefile, rakefile.rb, Rakefile.rb)
/Users/koichi/.rvm/gems/ruby-1.9.3-p392@octopress/bin/ruby_noexec_wrapper:14:in `eval'
/Users/koichi/.rvm/gems/ruby-1.9.3-p392@octopress/bin/ruby_noexec_wrapper:14:in `<main>'
(See full trace by running task with --trace)
"

…うん、実行するディレクトリを指定してないから、どこで実行してるかはわからないけど、 Rakefile が見当たらないのは当たり前だよなぁ。でも、octomacs ではきちんと Rakefile のあるディレクトリに行って実行しているっぽい気がする。じゃないと、他の人がツッコミを入れると思うし、作者もそんな凡ミスしたまま Marmalade にあげないよなぁ。何かそういう仕組みがあるはず。

でもまぁとりあえず、どこで実行してるかは知りたいので、スクラッチバッファで試してみる。

(shell-command-to-string "pwd")
"/Users/koichi
"

ホームディレクトリだった。

octomacs-rake-with-rvm にブレークポイントを貼って、その中で pwd 実行したらどうなるんだろ。

"/Users/koichi/my-blog\n"

うん、やっぱり何か、直接引数に渡さないで、外部からディレクトリを指定する仕組みがあるのね。

で、実はこれまでリファレンスを見ずに来たけど、それは shell-command-to-string って名前から何をするか大体想像がつくから。おそらく、引数の文字列をシェルにコマンドとして渡して、結果の文字列を返すのかなーって感じ。

で、この関数の機能の想像と、渡される引数と、スクラッチバッファでの実行結果を見て2つの疑問点がわいてきた。

  • 実行時にはどのシェルを使っていて、そのシェルの設定はどこから読み込んでるの?
  • 外部からディレクトリを指定する方法を知りたい

ついにリファレンスを見る時が来たようだ。

かいけつ!

GNU Emacs Lispリファレンスマニュアル: Synchronous Processes

上記リファレンスを見たのだが、

Function: shell-command-to-string command この関数は、シェルコマンドとしてcommand(文字列)を実行し、 コマンドの出力を文字列として返す。

と書いてあるだけ。全く予想通りだし、それ以上の事も書いて無かった。疑問点は何も解決しない。 「ついにリファレンスを見る時が来たようだ。」とか意気込んでおいてこの結末、残念すぎる。

こうなったらやることはググったり shell-command-to-string を使ってどんな動作をしているかを確かめるしかない。

とりあえず、

実行時にはどのシェルを使っていて、そのシェルの設定はどこから読み込んでるの?

はグーグル先生に聞いた所、 shell-command-to-string はログインシェルを使っているらしい事が分かった。で、ログインシェルを確認したら Bash だった。エラーメッセージで、bash が使われている理由が分かった。

僕は普段 zsh を使っているので、bash には rvm の設定をしていない。だったらログインシェルを zsh にすれば解決するだろう、という事で、ログインシェルを zsh にした。が、エラーメッセージは zsh でも command not found だから、問題は別の所にあるのだと考えた。で、次の式をスクラッチバッファで評価してみた。

(shell-command-to-string "echo $PATH")

そうすると ~/.zshrc で設定したパスが表示されていない。これで合点した。ホームディレクトリ以下の設定ファイルは読み込まないんだと…

/etc/zshrc に rvm のパスを設定するようにしたら、見事に動いた!これで解決したと言っても良いだろう。 ひとまずの解決策として、octomacs で rvm を使いたかったら、/etc/ 以下のログインシェルの設定に、rvm へのパスを追加すれば良い。

問題自体はこれで解決したが、残る疑問点を解決しておきたい。

Emacs Lisp の勉強

最後の疑問点はこれだ。

外部からディレクトリを指定する方法を知りたい

もしこんな指定方法があるとすれば shell-command-to-string を使っている箇所ではどこでも同じ事をしているはず。octomacs を眺めてみたら、ある共通点があった。

* `let` の中で実行されている
* `let` で `default-directory` を設定している

完全にdefault-directoryが怪しすぎる。とりあえず、グーグル先生に聞いてみたら、英語版のリファレンスマニュアルがヒット。

Synchronous Processes - GNU Emacs Lisp Reference Manual

そしてこんな記述。

The current working directory of the subprocess is default-directory.

ビンゴー!まさしくこれだった。 shell-command-to-string は最終的に call-process を読んでるだろうし、間違いないだろう。

で、イマイチこの辺の理解が曖昧だったのだけれども、どうやら default-directory は「バッファローカル変数」らしい。これは、「デフォルト変数」といういわばグローバル変数をバッファ毎に書き換えられるものらしい。(こんな理解でいいのかな)

ある関数の為に let でスコープを作り、その中で特定の変数に任意の値を束縛して関数の動作を制御するっていうこの Lisp のスタイル、面白いなーと思う。

終わり。

参考