unoh.github.com

続・Emacsを自分で拡張するためのTips

Wed Jul 02 22:26:50 -0700 2008


今年の春頃からトリプルディスプレイで仕事しているbokkoです。なんだか同僚の視線が気になりますが、あえて空気を読まないことにしています。


前に「EmacsLispを自分で拡張する際のTips」という記事を書きましたが、今回はその続きです。

EmacsLispは難しい?



EmacsLisp(以下、elisp)は難しいという意見をたまに耳にしますが、elisp自体はそれほど難しいものではありません。ただ、関数名がバラバラでややこしかったり、マニュアルが巨大でどこを見ていいのかわからず、目的のことをするための関数が見つからない、といったようにユーザが難しいと感じるのはelispという言語そのものではなく、環境(OS、ウインドウ、バッファなど)とのインタフェースにあるため、結果的にEmacsLispは難しいと感じてしまうことが多いようです。
実際、elispでプログラミングしていて感じるのはウインドウやバッファなどのオブジェクトを操作するのにどうやったらいいのかわからなくて途方に暮れるというものです。例えば、



というようなものがあります。調べ方やコツがわかってくれば割とすんなりいくことも多いのですが、慣れるまでは苦労すると思います。(僕もそうでした)

非対話的に引数で指定したファイルを開く



elispの拡張を書いていると、C-x C-fやM-x find-fileのように対話的にファイルを指定して開くのではなく、プログラム中で引数に指定したパスのファイルを開きたい場合があります。
それで以下のような処理を書いたとします。

(find-file-noselect "/home/bokko/test.txt")


名前からして↑の処理を実行するとファイルの内容が現在開いているバッファに表示されそうですが、実際にはそうなりません。この処理を実行すると、指定したファイルのバッファオブジェクトが返ってきます。目的のことをやるには今開いているバッファの内容をこのバッファに切り替える必要があります。バッファの切り替えにはswitch-to-buffer関数を使います。

(switch-to-buffer (find-file-noselect "/home/bokko/test.txt"))
;; もしくは
(switch-to-buffer (buffer-name (find-file-noselect "/home/bokko/test.txt")))


バッファからファイルに関する情報を取得する



現在開いているバッファのオブジェクトを取得するにはwindow-buffer関数を使います。

(window-buffer)


このオブジェクトからファイルの名前を取得するには、

(buffer-name (window-buffer))


とします。また、フルパスで取得するにはexpand-file-nameを使います。

(expand-file-name (buffer-name (window-buffer)))


外部プロセスを起動する



Emacsでは外部のプロセスを起動させることができます。既にあるシェルスクリプトなどを使いたい場合はstart-processのような関数を使うといいでしょう。映画生活では僕が去年ちょこちょこっと作ったCSSを縮小化するスクリプトを使っているのですが、僕の環境ではこれをEmacsから呼び出せるように↓のelispを読み込んでいます。

(defun compress-css ()
  "compress css"
  (interactive)
  (start-process "compress-css"
         "*compress-css*"
         "css-compressor")
  (message "all css file compressed."))


最後のcss-compressorが実行ファイル名です。ここではファイル名だけですが、実際にはパスの通った場所に置くかフルパスで指定します。また、この場合、実行結果は*compress-css*というバッファに吐き出されます。

補完入力をできるようにする



C-xC-f(find-file)でファイルを開くとき、Emacsでは補完入力ができます。去年ぐらいの頃にこれをどうやってやるのかわからず、結構悩んだ時期があったのですが、実は専用の関数が用意されているため、割と簡単にできます。以下のelispを実行すると、ミニバッファ内で補完入力ができるようになります。

(defvar tags-file-list '(
                 ("eigaseikatu" . "/home/bokko/eigaseikatu")
                 ("photozou" . "/home/bokko/photozou")
                 ))
(setq file (completing-read
   "Tags Key: "
   tags-file-list nil t "")) 


タブキーを押すと「eigaseikatu」と「photozou」が候補リストに表示されるようになり、入力した方の値がfileという変数に代入されます。前回紹介したvisit-tags-table-key.elではこの補完入力を行っていなかったため、毎回自分で正確に入力する必要がありましたが、今は上記のようにcompleting-readを使って補完入力をするようにしました。

elispで使う正規表現



elispで正規表現を扱う際は注意が必要です。というより、elispに限らず、プログラム中の正規表現を別言語に直接持って行く際には注意が必要です。というのもプログラミング言語で正規表現を扱う場合、その言語が正規表現をリテラルとして扱うのか文字列として扱うのか考慮する必要があります。特に文字列の場合はなにかと面倒です。elispでバックスラッシュにマッチさせるには、

\\\\


と書く必要があります。これはelispでは正規表現が文字列として扱われるので、余分にエスケープ処理が必要となるためです。sedやawk、もしくはPerlなどで正規表現を使ったプログラムを書いたことのある人には奇妙に思うかも知れませんが、elispやJavaのように正規表現をリテラルではなく、文字列として扱う言語ではこのようにエスケープを多用する必要があります。

デバッグ



elispのデバッグをする際はedebug-defunを使うのがいいでしょう。printfデバッグみたいなこともできますが、gdbみたいにステップ実行しながらデバッグすることができるので非常に便利です。
edebug-defunを使うには以下のelispを評価します。

(setq debug-on-error t)

あとはデバッグしたい関数の末尾でM-x edebug-defunと実行した後、その関数を実行すると、ステップ実行ができるようになります。スペースキーを押す度にミニバッファにelispの評価結果が表示されるので、実際にどのように動作しているのか把握しやすくなります。

目当ての関数を探し出す



先述したようにelispにはものすごくたくさんの関数があり、目的の関数を探し出すのが大変なのですが、実はelispで使える変数や関数はhelp-for-help関数(C-h)を使ってEmacs上で参照できるので、これである程度その大変さを和らげることができます。この関数を使うとUNIXのシステムコールをmanコマンドで調べるのと同じような感覚で関数の使い方について調べることができます。

ちょっとした応用



ここからは私が普段使っているelispのプログラムをちょこっと紹介します。

別のバッファで同じファイルを開く



プログラムを編集していると、同じファイルを複数のバッファから編集したくなるときがあります。例えば、同じファイル内にBという関数を呼び出しているAという関数があって、その両方を編集する必要があるような状況です。単純に同じファイルを同じバッファで開いてしまっても意味はないので、ウインドウを縦に分割して、2つのバッファに同じファイルの内容を表示してみます。

(defun open-same-file ()
  (interactive)
  (let ((same-file (find-file-noselect (buffer-name (window-buffer)))))
    (progn (split-window-horizontally)
    (other-window 1)
    (switch-to-buffer same-file))))


↑の関数を実行すると、C-x 3に割り当てられている関数であるsplit-window-horizontallyによってウインドウが縦に分割され、分割してできた新しい方に元々編集していたバッファの内容が表示されます。僕の環境では、以下のようにelscreen上の別のスクリーンで開くようにしています。

(defun elscreen-open-same-file ()
  (interactive)
  (let ((same-file (find-file-noselect (buffer-name (window-buffer)))))
    (elscreen-create)
    (switch-to-buffer same-file)))


C言語のヘッダファイルとソースファイルを関連づける



C言語でプログラムを書く際、関数の宣言と定義を分離するため、ソースファイル(.c)とは別にヘッダファイル(.h)を書くのが一般的です。そのため、ソースファイルとヘッダファイルとの間を行ったり来たリすることがあります。ずっと面倒だと思っていたので、以下のような関数を呼び出すだけで対応するファイルを開けるようにしました。

(defun c-open-relational-file (how-open-type)
  (interactive "nOpen-Type: ")
  (defun get-opened-file-name-prefix (file-name)
    (string-match "^\\([^.]+\\)\\.[^.]+$" file-name)
    (match-string 1 file-name))
  (defun get-ext-type (file-name)
    (string-match "\\.\\([^.]+\\)$" file-name)
    (match-string 1 file-name))
  (defun get-opening-file-name (file-name-prefix ext-list)
    (let ((opening-file-name (concat file-name-prefix "." (car ext-list))))
      (cond ((null (car ext-list))             nil)
            ((file-exists-p opening-file-name) opening-file-name)
            (t                                 (get-opening-file-name file-name-prefix
                                                                      (cdr ext-list))))))
  (let* ((ext-map '(
                    ("h" . ("c" "cpp" "cxx"))
                    ("c" . ("h" "s"))
                    ("s" . ("c"))
                    ("cpp" . ("hpp" "h"))
                    ))
         (opened-file-name (buffer-file-name (window-buffer)))
         (opened-file-name-prefix (get-opened-file-name-prefix opened-file-name))
         (opened-file-ext-type (get-ext-type opened-file-name))
         (opening-file-ext-type-list (cdr (assoc opened-file-ext-type ext-map)))
         (opening-file-name (get-opening-file-name opened-file-name-prefix
                                                   opening-file-ext-type-list))
         (opening-file-buffer (find-file-noselect opening-file-name)))
    (cond ((= how-open-type 1) (elscreen-switch-or-create opening-file-buffer))
          ((= how-open-type 2) (progn (split-window-horizontally)
                                      (other-window 1)
                                      (switch-to-buffer opening-file-buffer)))
          (t                   (message "Illegal Type")))))


この関数を実行すると、今編集しているファイルがtest.cというファイルの場合、test.hを別のバッファで開くことができます。最初にどう開くのかを数字で指定して、1の場合はelscreenを使って別のバッファで開くようにし、2の場合はウインドウを縦に分割して新しくできたバッファに対応するファイルを読み込みます。また、ソースファイルとヘッダファイルだけでなく、アセンブリコードのファイル(.s)も開けるようにしています(ただし、ヘッダファイルがある場合はそちらを優先します)。
上記のelscreen-switch-or-createは前回紹介したswitch-to-elscreen-createと同じものです。

追記(2009-01-05):

拡張子の直前以外で「.」があるとうまく関連ファイルを開けないバグを修正しました。ソースはこちら。

elファイルを再帰的にバイトコンパイルする



elispのプログラムはそのままでもEmacsが読み込むことができますが、バイトコードにコンパイルすることによって高速化することができます。大きな拡張だと、Makefileが用意されていて、(configure→)make→make installでインストールできるようになっているものもありますが、そうでないものも存在するため、たまに以下のelispを使って複数のelファイルを一気にコンパイルしています。

(defun my-byte-compile-directory ()
  (interactive)
  (defun byte-compile-directories (dir)
    (if (file-directory-p dir)
        (byte-compile-directory-r (mapcar (function (lambda (f) (concat dir "/" f)))
                                          (directory-files dir)))))
  (defun byte-compile-directory-r (file-list)
    (cond ((null (car file-list))
           nil)
          ((and (file-directory-p (car file-list))
                (not (string-match "/\.\.?$" (car file-list))))
           (byte-compile-directories (car file-list))
           (if (not (null (cdr file-list)))
               (progn
                 (byte-compile-directories (cadr file-list))
                 (byte-compile-directory-r (cdr file-list)))))
          ((string-match "\.el$" (car file-list))
           (progn
             (byte-compile-file (car file-list))
             (byte-compile-directory-r (cdr file-list))))
          (t
           (if (not (null (cdr file-list)))
               (byte-compile-directory-r (cdr file-list))))))
  (byte-compile-directories (replace-regexp-in-string "/$" "" default-directory)))


依存関係とかが複雑でないなら、バイトコンパイルしたい拡張のディレクトリに移動して↑の関数を実行するだけで全てのelファイルをバイトコンパイルしてelcファイルを生成することができます。

追記:(2008/7/3 18:20)

my-byte-compile-directory関数にバグがあったので修正しました。バグの内容ですが、再帰的にバイトコンパイルすると書いてあるのにサブディレクトリのelファイルをコンパイルできるようになっていませんでしたorz。上記の修正版ではちゃんとサブディレクトリのelファイルもバイトコンパイルされます。

参考文献



やさしいEmacs‐Lisp講座

おまけ



display3.png