flymake (いままでこれ無しでどうやってプログラム書いてたんだろう)

http://nya3.jp/tmp/flymake.png

id:nushioさんの日記(id:nushio:20071201)経由。

Eclipseではコードを書くと随時文法チェックが行われ、エラーのある行がエディタ上に表示される。Eclipseの便利な機能はたくさんあるけど、中でもこの機能はかなり強力で、いままでJavaのコードを書くときはこのためだけにEclipseを使っていたようなもの。

で、これをEmacs上でやってしまうのがflymake。id:nushioさんの記事で初めて知って、さっそく設定してみたのだけど、これは手放せそうにない。いままでどうやってEmacsでプログラムを書いてたんだか分からなくなってきてしまった。

というわけで、布教のために簡単な説明を。


とりあえず使ってみる

flymake.elはEmacs22以降では標準で付属しているらしい。場所はlisp/progmodes/flymake.el。Emacs21以前の場合は自分でflymake.elをインストールする必要がある。

で、とりあえず使ってみたい人は次のelisp.emacsに追加してみるとすぐに体験できるはず。

(require 'flymake)

(defun flymake-cc-init ()
  (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 "g++" (list "-Wall" "-Wextra" "-fsyntax-only" local-file))))

(push '("\\.cc$" flymake-cc-init) flymake-allowed-file-name-masks)

(add-hook 'c++-mode-hook
          '(lambda ()
             (flymake-mode t)))

.emacsを保存してEmacsを再起動したら、*.ccファイルを開いてみる。すると、文法ミスがある行に色がついているはず。

一般的な使い方

まず、

(require 'flymake)

しないと始まらない。あと、ソースコードを開いたときに自動的にflymake-modeが起動するように、適切なモードにフックを設定しておく。たとえば、C++の場合はこんなかんじ。

(add-hook 'c++-mode-hook
          '(lambda ()
             (flymake-mode t)))

これで、とりあえずflymake-modeは走るようになる。次に、ソースコードの各種類について、どんなコマンドラインで文法チェックを走らせるかを指定する。
ソースコードの拡張子と、それに対するコマンドラインの指定は、flymake-allowed-file-name-masks変数に格納されている。デフォルトではこんなかんじ。

(defcustom flymake-allowed-file-name-masks
  '(("\\.c\\'" flymake-simple-make-init)
    ("\\.cpp\\'" flymake-simple-make-init)
    ("\\.xml\\'" flymake-xml-init)
    ("\\.html?\\'" flymake-xml-init)
    ("\\.cs\\'" flymake-simple-make-init)
    ("\\.pl\\'" flymake-perl-init)
    ("\\.h\\'" flymake-master-make-header-init flymake-master-cleanup)
    ("\\.java\\'" flymake-simple-make-java-init flymake-simple-java-cleanup)
    ("[0-9]+\\.tex\\'" flymake-master-tex-init flymake-master-cleanup)
    ("\\.tex\\'" flymake-simple-tex-init)
    ("\\.idl\\'" flymake-simple-make-init)
    ...

flymake-simple-make-initは、コマンドラインelisp内で指定するかわりにmakeを使う。たとえば、flymake-simple-make-initを使ってCのコードを文法チェックしたかったら、ソースコードディレクトリの中に、以下のようなターゲットを記述したMakefileを置いておく。

.PHONY: check-syntax
check-syntax:
	gcc -Wall -fsyntax-only $(CHK_SOURCES)

これを置いておくと、flymake-simple-make-initは

make -s -C (ディレクトリ) CHK_SOURCES=(ソースファイル名) SYNTAX_CHECK_MODE=1 check-syntax

というコマンドラインを随時呼び出す。この結果を見て、ソースコードの彩色が行われる。

他の言語のデフォルトのコマンドラインは... 何やってるか知らないけど、よしなにしてくれるんじゃないかな。たぶん。

自分でハンドラを定義する

基本的には

(defun flymake-hoge-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-dir   (file-name-directory buffer-file-name))
         (local-file  (file-relative-name
                       temp-file
                       local-dir)))
    (list "hoge" (list "--option-1" "--option-2" local-file))))

という感じで定義すれば良いと思う。hogeなところとオプションは適宜設定。
あとは、定義したハンドラを登録しておく。

(push '(".+\\hoge$" flymake-hoge-init) flymake-allowed-file-name-masks)

例: Haskellの場合

EmacsWiki FlymakeHaskellを参考に少し手を加えてみた版。

; .emacs
(defun flymake-haskell-init ()
  (let* ((temp-file   (flymake-init-create-temp-buffer-copy
                       'flymake-create-temp-inplace))
         (local-dir   (file-name-directory buffer-file-name))
         (local-file  (file-relative-name
                       temp-file
                       local-dir)))
    (list "~/local/bin/flycheck_haskell.pl" (list local-file local-dir))))

(push '(".+\\hs$" flymake-haskell-init) flymake-allowed-file-name-masks)
(push '(".+\\lhs$" flymake-haskell-init) flymake-allowed-file-name-masks)
(push
 '("^\\(\.+\.hs\\|\.lhs\\):\\([0-9]+\\):\\([0-9]+\\):\\(.+\\)"
   1 2 3 4) flymake-err-line-patterns)

(add-hook 'haskell-mode-hook
          '(lambda ()
             (if (not (null buffer-file-name)) (flymake-mode))
          ))

あと、~/local/bin/flycheck_haskell.plに次のようなperlスクリプトを置いておく。(chompをやめたのは、Windowsで改行コードの問題からうまく動かなかったため。tempfileも削除しておいた。)

#!/usr/bin/env perl
# flycheck_haskell.pl

use strict;

### Please rewrite the following 3 variables 
### ($ghc, @ghc_options and @ghc_packages)

my $ghc = 'ghc'; # where is ghc
my @ghc_options  = ('-Wall');   # e.g. ('-fglasgow-exts')
my @ghc_packages = ();          # e.g. ('QuickCheck')

### the following should not been edited ###

my ($source, $base_dir) = @ARGV;

my @command = ($ghc,
            '--make',
            '-fno-code',
            "-i$base_dir",
            $source);

while(@ghc_options) {
    push(@command, shift @ghc_options);
}

while(@ghc_packages) {
    push(@command, '-package');
    push(@command, shift @ghc_packages);
}

open(MESSAGE, "@command 2>&1 |");
while(<MESSAGE>) {
    if(/(^\S+(\.hs|\.lhs))(:\d+:\d+:)\s?(.*)/) {
        print "\n";
        print $1;
        print $3;
        my $rest = $4;
        $rest =~ s/\s*$//s;
        print $rest;
        next;
    }
    elsif(/\s+(.*)/) {
        my $rest = $1;
        $rest =~ s/\s*$//s;
        print $rest;
        print " ";
        next;
    }
}
print "\n";
close(MESSAGE);

あとは、id:nushioさんのクールなエラー表示関数を追加すれば良いと思う。