Emacsで開発環境を整える

背景と動機

Emacsはエディタではない。環境である。

最近のIDEは非常に優秀で、IDEを使うか使わないかで生産性に大きな差が出てしまう、そういう状況が整いつつあるのかなという気がしています。 それでもやはり、動作の軽さやきめ細かいカスタマイズ性にひかれて、今でもEmacsを僕は愛用しています。Emacsはエディタであるにも関わらず、非常に高機能かつ多彩なカスタマイズが可能なため、人によっては「エディタではない、環境である」と主張する人もいます。実際、各種パッケージを導入することで、WebブラウジングIRCなど驚くほど多彩な機能が利用可能です。

そんなEmacsを使い続けて何年か経ち、設定ファイルもいろいろ貯まってきたので、今後気が向いた時に、Emacsでこんなこともできるよ!ということを紹介したいと思います。

ビルドと実行プロセスを簡略化するパッケージ

プログラミングを行う際、基本的なプロセスとして、主に「コードを書く」=>「コンパイル」=>「実行(デバッグ)」を繰り返すこととなります。今回はそのプロセスを簡略化するためのEmacsパッケージを紹介したいと思います。

yasnippet

コードを書くということを劇的に改善してくれるのがyasnippetです。 使い方を誤ると、コードクローンがそこら中に作られるよくないパッケージですが、よくあるイディオムを登録しておくと、毎度書く際の面倒臭さを軽減してくれます。

基本的なアイディアはdabbrev等と同じように略語を入力したらその略語を展開してくれるだけです。例えば、mainまで入力して「TAB」を押すと下記のようにコードが展開されます。

main[TAB]

==> 
int main(int argc, char *argv[])
{
  
  return 0;
}

僕は、下記のように設定しています。

;; yasnippet
;; http://code.google.com/p/yasnippet/
(when (locate-library "yasnippet-bundle")
  (require 'yasnippet-bundle)
  (setq yas/root-directory "~/.elisp/yasnippet/snippets")
  (setq yas/use-menu nil)
  (yas/initialize)
  (yas/load-directory yas/root-directory)
  ;;(setq yas/prompt-functions '(yas/x-prompt yas/dropdown-prompt))
  (setq yas/prompt-functions '(yas/dropdown-prompt)))

Flymake

最近のIDEは、文法間違いがあるとリアルタイムに指摘してくれます。その機能をEmacsで実現するのがFlymakeです。僕が使っている設定を下記に示します。 この設定をONにすると、C++のプログラムを書いている時にコンパイルエラー等があったらその箇所だけ赤く表示され、その箇所で「Ctrl+c d」というショートカットを使うとミニバッファにエラー内容が表示されます。 この機能を有効にしてからというもの、「プログラムを書く」=>「コンパイル」=>「コンパイルエラー」=>「プログラム修正」という繰り返しがなくなり、コーディング速度が上がったように思います。

;; flymakeパッケージを読み込み
(require 'flymake)
;; 全てのファイルでflymakeを有効化
(add-hook 'find-file-hook 'flymake-find-file-hook)

;; miniBufferにエラーを出力
(defun flymake-show-and-sit ()
  "Displays the error/warning for the current line in the minibuffer"
  (interactive)
  (progn
    (let* ((line-no (flymake-current-line-no) )
           (line-err-info-list (nth 0 (flymake-find-err-info flymake-err-info line-no)))
           (count (length line-err-info-list)))
      (while (> count 0)
        (when line-err-info-list
          (let* ((file (flymake-ler-file (nth (1- count) line-err-info-list)))
                 (full-file
                  (flymake-ler-full-file (nth (1- count) line-err-info-list)))
                 (text (flymake-ler-text (nth (1- count) line-err-info-list)))
                 (line (flymake-ler-line (nth (1- count) line-err-info-list))))
            (message "[%s] %s" line text)))
        (setq count (1- count)))))
  (sit-for 60.0))
(global-set-key "\C-cd" 'flymake-show-and-sit)

;; flymakeの色を変更
(set-face-background 'flymake-errline "red4")
(set-face-background 'flymake-warnline "dark slate blue")

;; Makefile が無くてもC/C++のチェック
(defun flymake-simple-generic-init (cmd &optional opts)
  (let* ((temp-file  (flymake-init-create-temp-buffer-copy
                      'flymake-create-temp-inplace))
         (local-file (file-relative-name
                      temp-file
                      (file-name-directory buffer-file-name))))
    (list cmd (append opts (list local-file)))))

(defun flymake-simple-make-or-generic-init (cmd &optional opts)
  (if (file-exists-p "Makefile")
      (flymake-simple-make-init)
    (flymake-simple-generic-init cmd opts)))

(defun flymake-c-init ()
  (flymake-simple-make-or-generic-init
   "gcc" '("-Wall" "-Wextra" "-pedantic" "-fsyntax-only")))

(defun flymake-cc-init ()
  (flymake-simple-make-or-generic-init
   "g++" '("-Wall" "-Wextra" "-pedantic" "-fsyntax-only")))

(push '("\\.c\\'" flymake-c-init) flymake-allowed-file-name-masks)
(push '("\\.\\(cc\\|cpp\\|C\\|CPP\\|hpp\\)\\'" flymake-cc-init)
      flymake-allowed-file-name-masks)

mode-compile

mode-compileEmacsに元々あったコンパイル機能を改善するパッケージです。 下記の設定をすることにより、ショートカット「Ctrl-c c」を使えば適切なコマンドを自動的に選択してコンパイルを行ってくれます。 実行されるコマンドはメジャーモードを元にしており、cperl-modeであればperlを実行し、c++-modeであれば「g++」 c-modeであれば「gcc」を使ってくれます。

;;;; mode-compile
(autoload 'mode-compile "mode-compile"
  "Command to compile current buffer file based on the major mode" t)
(global-set-key "\C-cc" 'mode-compile)
(autoload 'mode-compile-kill "mode-compile"
  "Command to kill a compilation launched by `mode-compile'" t)
(global-set-key "\C-ck" 'mode-compile-kill)

;; 全てバッファを自動的にセーブする
(setq mode-compile-always-save-buffer-p t)
;; コマンドをいちいち確認しない
(setq mode-compile-never-edit-command-p t)
;; メッセージ出力を抑制
(setq mode-compile-expert-p t)
;; メッセージを読み終わるまで待つ時間
(setq mode-compile-reading-time 0)

;; コンパイルが完了したらウィンドウを閉じる
(defun compile-autoclose (buffer string)
  (cond ((string-match "finished" string)
         (message "Build maybe successful: closing window.")
         (run-with-timer 0.3 nil
                         'delete-window
                         (get-buffer-window buffer t)))
        (t (message "Compilation exited abnormally: %s" string))))
(setq compilation-finish-functions 'compile-autoclose)

carun

ふと思いたって、自分で書いてみたelispです。 名前をつけてみましたが、パッケージのインストールは必要なく、下記のelispを直接.emacsに張り付ければOKです。

本格的にデバッグを行う時にはEmacsには「gdb」フロントエンドが入っているため、「M-x gdb」とコマンドを実行するとデバッガが起動されます。

ですが、printfデバッグで十分な時もあり、そういう場合はとにかく同じコードを細かく修正し、何度も実行することがよくあります。「Ctrl-c r」のショートカットを使うと、最初はコマンドを確認しますが(ディフォルトはファイル名から拡張子をとったもの)二回目以降は前回と同じコマンドを実行し、実行結果を表示してくれます。 「Ctrl-c Ctrl-r」を実行すると再度コマンドを確認します。

(defun carun-string-matches-internal (str n)
  (if (not (match-beginning n))
      ()
    (cons (substring str (match-beginning n) (match-end n))
          (carun-string-matches-internal str (+ n 1)))))

(defun carun-string-matches (match str)
  (if (not (string-match match str))
      ()
    (cons str
          (carun-string-matches-internal str 1))))

(defvar carun-command nil)
(defun carun-shell-command ()
  (shell-command carun-command "*Shell Command Output*" nil)
  (run-with-timer 1.0 nil
                  'delete-window
                  (get-buffer-window "*Shell Command Output*" t)))

(defun carun-exec ()
  (interactive)
  (let* ((carun-names (carun-string-matches "\\([^/]+\\)\\.\\([^\\.]+\\)$"
                                (buffer-file-name)))
         (carun-file (nth 1 carun-names))
         (carun-ext (nth 2 carun-names))
         (carun-comm (if carun-file (concat "./" carun-file) ""))
         (carun-read (read-string
                      "run-command: "
                      (if (not carun-command) carun-comm carun-command))))
    (setq carun-command carun-read)
    (carun-shell-command)))

(defun carun-exec-retry ()
  (interactive)
  (if (not carun-command)
      (carun-exec)
    (carun-shell-command)))

;; ショートカット設定
(global-set-key "\C-cr" 'carun-exec-retry)
(global-set-key "\C-c\C-r" 'carun-exec)

まとめ

コードを書く際に便利なパッケージとして「yasnippet」「Flymake」、コンパイルを簡単にしてくれるパッケージとして「mode-compile」、プログラムの実行を簡単にしてくれるパッケージとして「carun」を紹介してみました。

特定の環境に強く依存してしまうと、その環境以外では実力を発揮できないことがあります。こういう状況を「特定環境への過剰適応」と呼んでみます。 こういった「特定環境への過剰適応」を嫌い、カスタマイズ等を基本的にはしないという人もいて、その気持ちもよく理解できます。

例えば僕自身、サーバ設定等の作業を行う場合には、Emacsが入っていないことも多いので、カスタマイズなしの「vim」や「vi」を使うことがあります。そういった場合に「Emacsじゃないので実力が発揮できない」、と言うのは言い訳にしかなりません。

しかし、カスタマイズをすることで、生産性を50%上げることができるのであれば、それもまた自分の実力であり、時と場合に応じて積極的に利用するべきだと僕は思います。