Personal tools
You are here: Home ブログ matsuyama
Document Actions

imenu.el の使い方と応用

imenu とは

本の読者は、目次(インデックス)を見ることにより瞬時に読みたいページを開くことができますが、 imenu は編集中のバッファに対してインデックスを作成することにより、編集したい場所に瞬時に移動するのを補助してくれます。 TAGS のように、インデックスの作成機能があらかじめハードコードで実装されているわけではなく、ユーザーが独自のインデックス作成機能を追加できるように汎用的に実装されています [1] 。例えば、 C の関数定義のインデックス作成機能を追加することもできますし、 HTML の見出し(H1 〜 H6)のインデックス作成機能を追加することもできます。 15 年以上前に作成された Emacs Lisp ですが、そのシンプルさと拡張性さゆえに今でも十分便利に利用できます [2]

[1]Emacs Lisp や C のやや雑な実装はあらかじめ用意されています
[2]Emacs (Lisp) が長寿なのを、メリット(洗練される)とみるかデメリット(時代遅れ)とみるか、意見が分かれますが、僕は前者を支持します

imenu を使う

まず以下のようなプログラムを書いてみましょう。

test.c:

void hello() {
  printf("Hello");
}

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

次に、そのバッファで M-x imenu を実行してみましょう。 Index item とプロンプトされ、移動先の項目名を入力することができるようになります。ここで hello と入力して Enter を押すと hello 関数に移動することができます。関数が追加された場合などは、 M-x imenu RET *Rescan* RET を実行してインデックスを再構築する必要があります。基本的な使い方はこれだけです。シンプルですね。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu1.png/image

imenu はデフォルトでは関数や変数っぽいものを探してきてインデックスを作成します。 Emacs Lisp や C や Java など、大体の言語には対応しているようですが、そのままでは多少不便かもしれません。

anything から使う

M-x imenu は使いづらいので anything [3] から imenu を使うようにしてみましょう。

[3]anything については http://dev.ariel-networks.com/Members/matsuyama/open-anything-emacs を参照してください

anything で imenu を使うには以下の設定を ~/.emacs に書きます。

~/.emacs:

(require 'anything-config)
(setq anything-sources
     (list ...
           anything-c-source-imenu
           ...))
http://dev.ariel-networks.com/Members/matsuyama/images/imenu2.png/image

デフォルトで定義されている anything-c-source-imenu は深いインデックスに対応してなかったりするので EmacsWiki の anything-c-source-imenu を使用することをお勧めします。

http://www.emacswiki.org/cgi-bin/wiki/AnythingSources

which-func でさらに便利に

which-func は imenu のインデックスを用いて、現在どの関数を編集しているか(Which Function)をモードラインに表示してくれる機能です。 which-func という名前は、単に関数のインデックスがよく作られるからそのように名付けられたのですが、一般的には which-func はポイント付近にあるインデックスの項目名を表示する機能と言えます。

which-func を使うには以下の設定を ~/.emacs に書きます。

~/.emacs:

(require 'which-func)
; java-mode と javascript-mode でも which-func を使う
(setq which-func-modes (append which-func-modes '(java-mode javascript-mode)))
(which-func-mode t)
http://dev.ariel-networks.com/Members/matsuyama/images/imenu3.png/image

CEDET / Semantic との連携について

CEDET [4] / Semantic を使うと、標準のインデックスより詳細なインデックスを利用できるようになります [5]

[4]http://cedet.sourceforge.net/
[5]Semantic のパーザーが詳細なインデックスを作成してくれます

~/.emacs:

(require 'semantic-imenu)

標準の設定だと anything などから使った場合に読みにくい表示になるので以下のような設定を書いておくと良いかもしれません。

~/.emacs:

(setq semantic-imenu-summary-function
      (lambda (tag)
        (semantic-format-tag-summarize tag nil t)))

imenu を使いこなす

先述したように、 imenu はユーザーが独自のインデックス作成機能を追加できるように汎用的に実装されています。ここでは、その機能をフルに活用して作業効率を高める方法を紹介します。

ちなみに以下のコードが実行されていることが前提です。

(require 'cl)
(require 'imenu)

独自のインデックス作成機能を作る

imenu はインデックスを作成するために、バッファローカル変数にバウンドされた関数を呼びだそうとします。

最初に呼びだそうとするのは imenu-create-index-function バッファローカル変数の関数です。この関数は内部でインデックスを作成して返す関数である必要があります。インデックスの形式は (項目名 . ポイント) あるいは (項目名 . 子供) のシンプルなツリー構造です。

簡単な例を示します。まず以下のようなバッファを作ってみましょう。

test.txt:

* 見出し1
内容1
* 見出し2
内容2

次に *scratch* バッファなどで以下の関数を評価しておきましょう。この関数はバッファの先頭から * で初まる行を項目名としたインデックスを作成する関数です。

(defun outline-imenu-create-index ()
  (let (index)
    (goto-char (point-min))
    (while (re-search-forward "^\*\s*\\(.+\\)" (point-max) t)
      (push (cons (match-string 1) (match-beginning 1)) index))
    (nreverse index)))

そして test.txt のバッファで M-: (setq imenu-create-index-function 'outline-imenu-create-index) とやって、 imenu からこの関数が呼ばれるようにします。最後に M-x imenu RET *Rescan* RET とすれば独自のインデックス作成機能が無事追加されました。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu4.png/image

text-mode の場合に、このインデックス作成機能を使いたい場合は以下の設定を ~/.emacs に書いておくと良いでしょう。

~/.emacs:

(add-hook 'text-mode (lambda () (setq imenu-create-index-function 'outline-imenu-create-index)))

さて、 imenu が imenu-create-index-function バッファローカル変数に関数がバウンドされていないと判断すると、今度は imenu-prev-index-position-function バッファローカル変数と imenu-extract-index-name-function バッファローカル変数を使ってインデックスを作成しようとします。これらにバウンドされた関数は次の手順で呼び出されます。

  1. バッファの最後にポイントを移動する
  2. imenu-prev-index-position-function バッファローカル変数の関数を呼び出し、一つ前の項目に移動する
  3. imenu-extract-index-name-function バッファローカル変数の関数を呼び出し、項目名を取得する
  4. 2 が nil を返すまで 2 〜 3 を繰り返す

この手順にそって imenu がインデックスを構築していきます。前の例をこの方法で実装してみましょう。

まず * の前に移動する関数を定義します。

(defun outline-imenu-prev-index-position ()
  (re-search-backward "^\*\s*.+" (point-min) t))

次に項目名を取得する関数を定義します。

(defun outline-imenu-extract-index-name ()
  (if (looking-at "^\*\s*\\(.+\\)")
      (match-string 1)))

最後に test.txt のバッファで M-: (setq imenu-create-index-function 'imenu-default-create-index-function imenu-prev-index-position-function 'outline-imenu-prev-index-position imenu-extract-index-name-function 'outline-imenu-extract-index-name) とやって、 imenu からこれらの関数が呼ばれるようにします。 M-x imenu RET *Rescan* RET すればちゃんとインデックスが作成されていると思います。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu5.png/image

どちらの方法が使いやすいかはケースバイケースだと思いますが、基本的には後者でインデックスを作成し、複雑な処理を行う場合は前者でインデックスを作成すれば良いでしょう。

以上を踏まえて実用できる応用例を二つあげておきます。是非参考にしてください。

応用1. HTML の見出しのインデックス作成機能を作る

HTML の見出しでインデックスを作成できるようにしてみましょう。これは上の例で作ったものを少し変更にしただけです。

(defun html-imenu-create-index ()
  (let (index)
    (goto-char (point-min))
    (while (re-search-forward "<h[1-6]>\\(.+?\\)</h[1-6]>" (point-max) t)
      (push (cons (match-string 1) (match-beginning 1)) index))
    (nreverse index)))

以下のような設定を書いておくと良いでしょう。

~/.emacs:

(add-hook 'html-mode (lambda () (setq imenu-create-index-function 'html-imenu-create-index)))
(setq which-func-modes (append which-func-modes '(html-mode)))

which-func を有効にしておくと、編集中のセクションがすぐに分かって便利です。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu6.png/image

応用2. JavaScript のメソッドのインデックス作成機能を作る

さらに実用的なものを紹介しましょう。一般的に JavaScript でのメソッドへの移動は TAGS を使ってもなかなかうまく実現できません [6] 。なぜなら

function foo() {
  ...
}

という定義はうまく処理できても、 prototype.js でのクラス定義やオブジェクト記法でのメソッドはうまく処理できないからです [7]

// prototype.js のクラス定義
var Foo = Class.create({
  bar: function() {
    ...
  }
});

// オブジェクト記法での関数定義
// 名前空間として使ったりする
var Foo = {
  bar: function() {
    ...
  }
};
[6]http://d.hatena.ne.jp/secondlife/20060904/1157354851 という方法もありますが
[7]もちろんパーザーが対応すればうまく処理できますが、言語仕様でもないものをパーズすべきかというと微妙な話です

さて、上のような一見面倒くさそうな記述をうまくパーズして、メソッドのインデックスを作成できるようにしてみようと思いますが、もちろん厳密にパーズするわけではありません。このような処理は適当にやるのが効果的なのです。今回は以下のような手順でやってみましょう。

  1. Class.createObject.extend 、オブジェクト記法を検出して、定義される対象を項目名とする
  2. そこから { を検索し、対の } までが、その対象でのメソッド定義可能範囲と前提する
  3. { から } までで、 name: function () となっている name をメソッド名としてインデックスに追加する

以上のことをやれば、それなりにちゃんとした JavaScript のメソッドのインデックスを作ることができます。

実際のコードは以下のようになります。

;; 識別子の正規表現
(defvar javascript-identifier-regexp "[a-zA-Z0-9.$_]+")

;; } までの class のメソッドを列挙する関数
(defun javascript-imenu-create-method-index-1 (class bound)
  (let (result)
    (while (re-search-forward (format "^ +\\(\%s\\): *function" javascript-identifier-regexp) bound t)
      (push (cons (format "%s.%s" class (match-string 1)) (match-beginning 1)) result))
    (nreverse result)))

;; メソッドのインデックスを作成する関数
(defun javascript-imenu-create-method-index ()
  (cons "Methods"
        (let (result)
          ;; $name = Class.create
          ;; $name = Object.extend
          ;; Object.extend($name,
          ;; $name = {
          ;; をクラスあるいはオブジェクトとする
          (dolist (pattern (list (format "\\b\\(%s\\) *= *Class\.create" javascript-identifier-regexp)
                                 (format "\\b\\([A-Z]%s\\) *= *Object.extend(%s"
                                         javascript-identifier-regexp
                                         javascript-identifier-regexp)
                                 (format "^ *Object.extend(\\([A-Z]%s\\)" javascript-identifier-regexp)
                                 (format "\\b\\([A-Z]%s\\) *= *{" javascript-identifier-regexp)))
            (goto-char (point-min))
            (while (re-search-forward pattern (point-max) t)
              (save-excursion
                (condition-case nil
                    ;; { を探す
                    (let ((class (replace-regexp-in-string "\.prototype$" "" (match-string 1))) ;; .prototype はとっておく
                          (try 3))
                      (if (eq (char-after) ?\()
                          (down-list))
                      (if (eq (char-before) ?{)
                          (backward-up-list))
                      (forward-list)
                      (while (and (> try 0) (not (eq (char-before) ?})))
                        (forward-list)
                        (decf try))
                      (if (eq (char-before) ?}) ;; } を見つけたら
                          (let ((bound (point)))
                            (backward-list)
                            ;; メソッドを抽出してインデックスに追加
                            (setq result (append result (javascript-imenu-create-method-index-1 class bound))))))
                  (error nil)))))
          ;; 重複を削除しておく
          (delete-duplicates result :test (lambda (a b) (= (cdr a) (cdr b)))))))

ついでに単純な関数のインデックスも作る関数も定義しておきます。

(defun javascript-imenu-create-function-index ()
  (cons "Functions"
         (let (result)
           (dolist (pattern (list
                             (format "\\b\\(%s\\) *= *function" "function \\(%s\\)"
                                     javascript-identifier-regexp
                                     javascript-identifier-regexp)))
             (goto-char (point-min))
             (while (re-search-forward pattern (point-max) t)
               (push (cons (match-string 1) (match-beginning 1)) result)))
           (nreverse result))))

あとは以下の関数を定義して、

(defun javascript-imenu-create-index ()
  (list
   (javascript-imenu-create-function-index)
   (javascript-imenu-create-method-index)))

適当な JavaScript ファイルで M-: (setq imenu-create-index-function 'javascript-imenu-create-index) とやります。

test.js:

var Foo = Class.create({
  bar1: function() {},
  bar2: function() {}
});
http://dev.ariel-networks.com/Members/matsuyama/images/imenu9.png/image

ちゃんとパーズできてるみたいです。

ちなみに prototype.js 1.6.0.2 だとこんな感じになります。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu7.png/image

anything から使うともっと便利になります [8]

[8]EmacsWiki の anything-c-source-imenu を使っています
http://dev.ariel-networks.com/Members/matsuyama/images/imenu8.png/image

最後は以下のような設定を ~/.emacs に書いておけば良いでしょう。

~/.emacs:

(add-hook 'javascript-mode-hook (lambda () (setq imenu-create-index-function 'javascript-imenu-create-index)))

これで prototype.js のクラスやオブジェクト記法のメソッドにも簡単に移動できるようになりました。すばらしい。

最後に

imenu はシンプルですが非常にパワフルな Emacs Lisp です。独自の構文を持つファイルや、複雑な階層構造を持つファイルを編集するときに、移動に手間をとられるようだったら是非 imenu を使うのを検討してみましょう。一度、インデックス作成機能を作ってしまえば、 which-func で現在編集中の場所が瞬時に分かりますし、 anything などと組み合わせると移動に手間をとられるということはほとんどなくなると思います。

Category(s)
emacs
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/imenu/tbping

GNU Screen の縦分割を使ってみた

GNU Screen の CVS HEAD に縦分割が入っているらしい [1] ので早速導入してみました。

[1]http://lists.gnu.org/archive/html/screen-users/2007-02/msg00000.html
$ cd ~/src
$ cvs -z3 -d:pserver:anonymous@cvs.savannah.gnu.org:/sources/screen co screen
$ cd screen/src
$ ./configure --prefix=$HOME --enable-colors256
$ make && make install
$ ~/bin/screen

$HOME にインストールすると若干おかしなことになるみたいですが、一応動いているようです(たぶん以前にインストールしたファイルが使われている)。

^A | で縦分割できます。横分割に比べて作業スペースの利用効率が高くてなかなか良いです。

screen-vsplit.png

Category(s)
linux
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/gnu-screen-vertical-split/tbping

Re:GNU Screen の縦分割を使ってみた

Posted by matsuu at 2008-04-17 01:59
gentoo環境なら、app-misc/screen-4.0.3_p20070403が縦分割対応してます。今はまだp.maskされてますが。

Re:GNU Screen の縦分割を使ってみた

Posted by matsuyama at 2008-04-19 22:41
portage のすごさを実感しました。

iPod Touch の音楽を Linux で管理する


iPod touch を買ったはいいけど音楽を聴くのにすら使ってなかった( iTunes なんて使いたくない)ので、いろいろ頑張って最低限使えるレベルまでもってきました。今回はそのメモ。

iPod touch の音楽を Linux で管理する場合、以下のような問題が発生します。

  1. Windows でリッピングした mp3 のタグのエンコーディングは ShiftJIS なので iPod touch で見たときに文字化けする
  2. 微妙なタグの違いを修正する必要がある(アーティスト名とか)
  3. ogg や flac などで音楽を管理している場合は、転送時に mp3 や aac に変換する必要がある
  4. iPod touch のマウントと曲の転送方法

これらを解決しないと快適な iPod touch は送れません。以下、順番に僕がやった解決策を紹介します。

ちなみに実際以下の方法を試す場合はバックアップを取っておいてください。僕は責任をとれません。

$ cp -a ~/music ~/backup/music

ShiftJIS なタグを UTF-8 に変換する

Windows で音楽 CD をリッピングした場合、仕様外であるにもかかわらず、 ShiftJIS なタグをつっこみます。昔リッピングした mp3 はほとんどそのようになっているので、まずそれを変換する必要があります。膨大な量の mp3 があるので、(仮にあったとしても) GUI なソフトウェアでやるのはつらいような気がします。なので、文字コードを推測してユーザーの操作を最低限に抑える一括変換スクリプトを書きました。添付されている fixtag というスクリプトをダウンロードしてきて以下を実行します。

$ find ~/music -type f -name '*.mp3' -print0 | xargs -0 ./fixtag

文字コードが一意に認識できない場合は、ユーザーに問いかけて修正していきます。僕の環境ではほとんど自動でやってくれました。 id3info コマンドを使ってうまく変換できているか確かめることができます。

$ echo $LANG
ja_JP.UTF-8
$ id3info ~/music/Mr.Children/Kind\ of\ Love/01\ -\ 虹の彼方へ.mp3 | grep TIT
=== TIT2 (Title/songname/content description): 虹の彼方へ

微妙な違いのあるタグを修正する

例えばアーティスト名に微妙な違い(一方は小文字で一方は大文字など)がある場合、 iPod touch は両者を違うものとして認識します。なのでその辺りをうまく修正する必要があります。こういうのは GUI が便利なので EasyTag [1] というソフトウェアを使って修正しました。微妙に動作が怪しいところもありますが、なんとかなります。ちなみに Gentoo なら、

# emerge easytag

でインストールできます。

[1]http://easytag.sourceforge.net/

ogg や flac で音楽を管理している場合

ogg や flac で音楽を管理している場合、 iPodtouch に転送するときに再生可能な形式に変換する必要があります。僕は音楽を flac で管理しているのですが、 mp3fs という丁度良いソフトウェアがあったのでこれを利用しました。 mp3fs は flac 形式のファイルを FUSE 経由で mp3 として認識できるようにしてくれるソフトウェアです。 Gentoo なら、

# emerge mp3fs

でインストールできます [2] 。ただ僕の場合、さらに特殊なことをやっていて、タグを flac の独自形式で記録せずに flac ファイルの先頭に ID3v2 タグをいれています [3] 。残念ながら、そのようなことをしている場合に mp3fs が mp3 変換時にタグをロストしてしまうので、パッチを書いてなんとかしました。(たぶん必要とする人はいないでしょうが)使用したい場合は添付されているパッチをダウンロードしてきて以下のコマンドを実行してください。 libid3 と libid3tag が必要です。

[2]さりげなく Gentoo の寄さをアピール
[3]仕様的には許可されます
$ cd ~/tmp/mp3fs-0.12
$ patch -p1 < ~/tmp/mp3fs-0.12-id3.patch
$ ./configure --prefix=$HOME
$ make && make install

iPod touch に転送する際は mp3fs でマウントしたディレクトリを使います。

$ ~/bin/mp3fs ~/music,192 ~/mnt/music

iPod touch のマウントと音楽の転送

iPod touch を USB 経由でマウントする方法がわからなかったので sshfs でマウントします。 あらかじめ iPod touch を Jailbreak して ssh を入れておいてください。

$ # 192.168.1.2 は iPod touch の IP アドレス
$ # Terminal で ifconfig して確認してください
$ sshfs root@192.168.1.2:/var/root/Media ~/mnt/ipod

Gtkpod [4] というソフトウェアを使って音楽を転送します。 ~/mnt/ipod で iPod を追加するだけで OK です。後は ~/mnt/music から適当に音楽ファイルを追加して「変更を保存」します。最後に iPod touch を再起動すれば音楽ファイルが追加されていると思います。

[4]http://www.gtkpod.org/

最後に

ここまでやるのに数ヶ月かかったのは秘密です。主にやる気との闘いだったのですが、何気に mp3 のタグが障壁になったりして大変でした。やっぱり統一した管理というのは重要ですね。

fixtag fixtag
Size 3.9 kB - File type application/octet-stream
mp3fs-0.12-id3.patch mp3fs-0.12-id3.patch
Size 11.6 kB - File type text/x-patch
Category(s)
linux
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/ipod-touch-with-linux/tbping

strstr 関数の haystack と needle

$ man strstr
char *strstr(const char *haystack, const char *needle);

オブジェクト A からオブジェクト B を見つける関数を書く場合、オブジェクト A に haystack 、オブジェクト B に needle という引数名を付けるのは PHP の文化だと思っていたのですが、どうやら UNIX の文化だということが判明しました。少し調べてみた感じだと「 Needdle in a haystack 」という英語のイディオムが元ネタになっているらしくて、意味は「見つけるのが困難なぐらい大きなオブジェクト (haystack) の中にあるオブジェクト (needle) 」らしいです [1] 。直訳だと「干し草の山の中の針」です。こういう味のある名前は好きですが、ボキャブラリが低いとさっぱり意味がわからないという弊害を伴います。

[1]http://en.wikipedia.org/wiki/Needle_in_a_haystack
Category(s)
offtopic
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/haystack-and-needle/tbping

Re:strstr 関数の haystack と needle

Posted by Anonymous User at 2008-05-23 21:37
まあ、わらと針という意味がわかっていれば、どちらが検索対象になるのかはわかりやすいかと思います。
という私も最初はhaystackという聞きなれない単語に戸惑いましたが。

zsh のコマンドラインに入力中のファイルを閲覧する方法

シェルというのはコンピュータに命令を送るのには非常に適したインターフェースですが、その反面、操作する人間のちょっとした気分の変化にうまく対応できないという欠点があります。

$ svn add foo.txt

ここでちょっと気が変わって foo.txt の中身を見たいとします。僕の場合は以下のようにしていました。

$ svn add foo.txt^u # Ctrl+U でコマンドラインをクリア
$ less foo.txt
$ ^y # ^u した内容をペースト
$ svn add foo.txt

これではシェルを使っているのではなく、シェルに使われてしまっています。せっかく zsh を使っているのですから以下のような事をしたいです。

$ svn add foo.txt^x^r # Ctrl+X Ctrl+R でカーソル直前のファイルを less
... # less 中
$ svn add foo.txt # less が終わったら入力していたコマンドラインを復元

ちょっと調べてみた感じだと結構簡単に実装できるようです [1]

[1]例のごとく zsh のドキュメントはあまり役にたちませんでした
function view-file() {
    zle -I
    local file
    local -a words

    words=(${(z)LBUFFER})
    file="${words[$#words]}"
    [[ -f "$file" ]] && $PAGER "$file"
}
zle -N view-file
bindkey "^x^r" view-file

これを ~/.zshrc あたりに記述しておけば Ctrl+X Ctrl+R でコマンドラインに入力中のファイルを閲覧することができます。ついでに編集できるようにもしておきましょう。

function edit-file() {
    zle -I
    local file
    local -a words

    words=(${(z)LBUFFER})
    file="${words[$#words]}"
    [[ -f "$file" ]] && $EDITOR "$file"
}
zle -N edit-file
bindkey "^x^f" edit-file

これで、 Ctrl+X Ctrl+F でコマンドラインに入力中のファイルを編集できます。ただ EDITOR が Vim の場合は少し問題があるようで、適宜 jed なり emacsclient なりに変更して使ってください。

Category(s)
linux
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/zsh-peek-file/tbping

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.