ctagsを使ってVimでCode Readingを加速する


2010年 12月 14日

コードを書いて何か具体的なものを作ることが、僕たちプログラマの仕事でありアイデンティティですが、
ただ、一日中コードを書き続けているわけではありません。
優れたコードを書くスキルを習得するためには、いろんな人たちが書いたコードを大量に読むことが欠かせません。

この、コードを読む(Code Reading)ことはなかなか奥が深く、読みこなすには骨が折れるものです。
なぜならその背景をコードから読み解く知識(データ構造、アルゴリズムやアーキテクチャ)や経験が要求されるからです。
しかし、Code Readingは何よりも楽しいものです。読み進めていると斬新なアイデアや綺麗に表現しているコードに出会ったときは興奮してしまいます。

では、コードはどうやって読むのでしょうか?
印刷して紙で読む? Webブラウザでsyntax highlightされたコードを読む?

いいえ。エディタです。

使い慣れたエディタは書くときにも最高に真価を発揮してくれますが、読むときにも便利な機能を提供してくれています。

今回は僕がVimでコードを読むときの一連の流れを紹介します。
EclipseやNetBeansをはじめとするIDEでは簡単にコード間を移動できますが、Vimを使う場合には少し手間をかけてあげなければなりません。
クラスや構造体、メソッドや関数、マクロといったものものに対してタグ(インデックス)をつけて、Vimがソースファイルのどこに移動すればよいか教えるわけです。

さて、このタグですが、どうやって作るのでしょうか?
タグファイルはテキストファイルなのでフォーマットにしたがって手で作ってもいいですが、Exuberant ctagsを使って自動化するのが一般的だと思います。
helpの中でもこのctagsを使ってタグファイルを生成する例が示されています。
対応しているプログラミング言語も豊富です。

UnixでもWindows(cygwin)でもソースコードからビルドして使用できます。
ctagsの本家からlatestのソースコードを取得してビルドしましょう。

$ wget "http://prdownloads.sourceforge.net/ctags/ctags-5.8.tar.gz"
$ tar zxf ctags-5.8.tar.gz
$ cd ctags-5.8
$ ./configure --prefix=/path/to/install-dir;
$ make; make install

さぁこれでタグファイルを生成できるようになりました! 早速作ってみましょう。
tagsファイルがどうやって作られるのか気になりませんか? よろしい。ctagsのコードを少し覗いてみることにしましょう。

$ ctags -R .

ctagsが実行コマンドになります(etagsコマンドはEmacs用のタグファイルを生成します)。
オプションを組み合わせてタグファイルを生成することもできますが、上記の用に何もオプションを付けずに実行するだけで十分です。
ただ、生成するタグファイルをカスタマイズしたい場合や、ctagsが対応していないプログラミング言語のソースコードに対してタグファイルを生成したい場合はオプションを指定しなければなりません。
主なオプションは以下のとおりです。

  • -R --recurcive

    指定したディレクトリか実行したカレントディレクトリから再帰的にファイルを探索してコードを解析します。
  • --list-kinds[=LANG]

    指定した言語(LANG)に対するタグ種別を表示します。その多くは言語で定義されている構文と対応しています。たとえば、Cであれば関数、構造体、列挙対、マクロ等が定義されています。Rubyであればクラス、メソッド、モジュール、シングルトンが定義されています。ctagsを実行するときに何も指定しなければ表示したもので[off]がついていないものが適用されます。
  • --<LANG>-kinds=[+|-]kinds

    指定した言語(LANG)をパースするときのタグ種別を指定します。--list-kindsでその言語のタグ種別を調べて、有効にしたいタグ種別を指定します。
  • --list-languages

    ctagsがサポートしている言語を表示します。
  • --langdef=name

    ユーザが独自に言語を定義できます。以下の--langmapオプションで、どのような拡張子をパース対象とするファイルかを指定し、--regex-<LANG>オプションでどのパターンをタグの定義とするのかをあわせて指定して使います。
  • --langmap=name:extention[,name:extention]

    指定した言語(name)に対応付けるファイルの拡張子(extenion)を指定します。
  • --regex-<LANG>=/regexp/repalcement/[kind-spec/][flags]

    指定した言語(LANG)において指定した正規表現でタグを定義できます。
    LANGはctagsに組み込まれている言語や--langdefで指定した言語を指定します。
    解釈される正規表現はデフォルトで拡張POSIX正規表現です。

    例えば、Rubyはctagsでサポートされていますが、’定数’のタグ種別は定義されていません。
    そこでctagsでパースするときに次の用に指定すると定数にもタグを作成してくれるようになります。
$ ctags -R --regex-ruby=/^[ \t]*([A-Z_][A-Z0-9_]*)[ \t]*=/\1/C,constant/ .

では、ctagsのソースファイルに対するタグファイルが生成されたところでVimでmain.c開いてctagsを探検していきましょう。
ctagsのような単一の実行プログラムの動作を知るにはエントリポイントからたどっていくのが定石です。
Cで記述されているコードならmain.cや<実行ファイル名>.cなどがエントリポイントになっていることが多いです。
そういうファイルが見当たらなければ、grep '^int main('grep '^main'等などgrepを駆使して探します。

さてctagsのエントリポイントはどうなっているでしょうか。

main関数の中の初期化処理や終了処理を省くとこのようになります。だいぶすっきりして読みやすそうですね。
makeTags()関数がどうやら名前からして主体の実行処理だと当たりをつけて読み進んでいきます。

さて、Vimでコードの定義先に移動するにはどうすればいいのでしょうか?

カーソルをmakeTags()関数上に移動させCTRL-]で定義先にジャンプできます。
また、Ctrl-Tでジャンプ先から戻れます。

仕組みとしては、先ほど作成したタグファイルから関数名を検索して、マッチした箇所に移動しています。
また、タグジャンプすると、ジャンプ先のタグとジャンプ元の情報がタグスタックと呼ばれるスタックに詰まれていき、戻るときにはそのスタックからポップするというわけです。
基本はこの2つさえ覚えておけばVimでコード間を移動できます。

あとは重複してタグがマッチする場合もあります。
:ts[elect]コマンドで、マッチしたタグを一覧表示することができます。
:tnextで別のタグにジャンプでき、:tpreviouseでその前のタグにジャンプできます。
では、コード間を移動できるようになったので先へ読み進めましょう。

makeTags()の中の処理はオプションによって分岐しています。今回はctagsを実行するときに単一のソースファイルを指定したと想定してcreateTagsFromListFile()関数を見ていきましょう。

ここでは、パース対象となるソースファイルをオープンしてファイルハンドラを生成しています。

ファイルハンドルから対象のファイルをオブジェクトに変換してを元にタグを生成していきます。
このあたりはいろいろごちゃごちゃしてて読みづらいですが、とりあえず主体の処理である関数を名前から推測して追っていくことが全体を俯瞰する場合、合理的だと思います。
深さ優先的に関数やマクロを読んでいく方法もありますが、いつまでたっても主体の処理を追えないので、僕はこうしています。

さてparseFile()ではgetFileLanguage()でパース対象のファイルから言語を調べています。
manによるとctagsではソースファイルの拡張子、ソースファイル名、shebangの3つの方法から言語を判断しているとあります。ここでは追いませんが、getFileLanguage()を追うとそのように言語を特定していることがわかります。

このcreateTagsForFile()が主体となります。

LanguageTableというテーブルから言語を選択して、パーサを実行していることがこのコード片からわかります。
lang->parser()のparserからジャンプするとparse.hのparserDefinition構造体で定義されているsimpleParserメンバにぶつかると思います。
simpleParserからさらに飛ぶとvoid 型を返す関数の関数ポインタであるとtypedefで定義されています。

ここでCにおけるインタフェースとテーブル駆動型パーサの実装の一例を学べます。
parse.hではパーサの型(parserDefinition)を宣言しておき、各言語でその型に従いパーサを実装し、各種の言語パーサテーブルから任意のパーサオブジェクトを取得して実行することがこれらのコード片から読み取れます。

Rubyを例に見てみることにします。ruby.cを覗いてみましょう。parseDefinition型を返す関数がパーサの実装のはずです。

ありましたね。ファイルの末尾に定義してあるRubyParser()がそれです。

ここでparserDefinitionオブジェクトを生成してRubyのパーサ関数であるfindRubyTags()を登録しています。
したがって、lang->parser()ではこの関数がコールされ、Rubyのソースファイルをパースします。

お疲れ様でした。これでctagsがどうやってtagsファイルを作成しているか俯瞰できたと思います。
ここまでコードリーディングが進めば、たとえばサポートされていない言語をctagsに組み込みたい、と思ってハックしたくなれば<言語名>.cというファイルを作り、宣言されている型にしたがってパーサを実装すればよいことになります。

どうでしょうか、早足でしたがコードの探検はとってもわくわくしませんでしたか?
コードを書くときにはある種の恐怖心が付きまといます。
バグを作ってしまうのではないか、きれいにかけないんじゃないか、等々。
そんなときはどんどんコードを読んでみましょう。巨人たちが苦心して作り上げたものから学ぶのです。

そして、書く。

ctagsさえあればコードに対してとりあえずはタグ付けしコード間を移動できるようになるので、あとはアルゴリズムやデータ構造、アーキテクチャを勉強しつつどんどん読んでいきましょう。
読めば読むほど経験が身につき読むスピードが加速され、より多くのコードを読みこなせるようになり、そしてコードを書くときのアイデアの糧となります。