最低限必要となるツール

コードリーディングに最低限必要と考えらえる3つのツールについて説明します。

ソースコードファイルの閲覧
less
文字列検索
grep
ソースコードツリーの巡回
find, xargs

この演習は様々なツールの使い方を覚えることを目的とはしていません。地道にフローを追うというコードリーディングの本質を体験してもらうために、基本的なツールについてだけ説明することにしました。

コードリーディングを実践している他者に聞いてみた経験では、読解者のリーディングのスタイルによって好まれるツールは違うようです。またプラットフォームや対象となるソースコードの記述に使われている言語によっては利用できないツールもあります。基本的なツールはスタイルによらず役に立ちます。また多くのプラットフォームで利用可能で、しかも言語を選びません。

ソースコードファイルの閲覧

もし使い慣れたテキストエディタがあれば、それを使って閲覧するのが良いでしょう。Fedoraでは主要なエディタとして、emacs, gedit, viが利用できます。エディタを使う場合、オープンした読解の対象となるソースコードファイルを意図せず変更して、さらに保存してしまうことが無いように注意して下さい。ファイルをリードオンリーモードで読み込む、あるいはchmodコマンドを使ってファイル自体の書き込み権限を削除しておくと良いでしょう。

もし使い慣れたテキストエディタがなければ、ページャー(less)が使えます。筆者の同僚でソースコードをゴリゴリと読む人のなかには使い慣れたテキストエディタがあるにもかかわらずlessを使っている人もいます。

lessコマンドは引数で指定したファイルを開きます。 +Nというオプションを追記すると開いた直後にファイルの先頭を表示するかわりに、N行目から表示を開始します。以降に延べる通り、grepによる文字列検索の結果には、その文字列が出現した行の行番号が番号が含まれます。 +Nオプションは、その照合箇所を確認するのに便利です。

foo.cの50行目以降を表示する例:

$ less +50 foo.c

ファイルを開いた後にlessは対話的な操作を受けつけます。以下に特に重要なものを列挙します。

q
終了
SPACE(あるいは^F)
次ページへ移動
^B
前ページへ移動
/
文字列検索(前向き)
?
文字列検索(後ろ向き)
n
次の検索結果
N
前の検索結果
N g
ファイル中の*N* 行目へ移動

文字列検索

grepコマンドを使うと引数で与えたファイル群(FILES)に対してまとめて文字列検索をかけることができます。

grep [OPTIONS] -n -e PATTERN FILES...

FILESで指定したファイル群に対してPATTERNとマッチする行を表示します。PATTERNには正規表現を指定します。後述する正規表現のメタ文字を含まない限り文字列をそのまま指定すれば、それもまた正規表現のサブセットとしてそのまま検索できます。検索の結果として、指定したファイル群に対して、その文字列を含むファイル名、行番号、その行を列挙できます。ただし検索文字列をシングルクォートで囲んだ方が良い場合があります。PATTERN中の文字列がシェルのメタ文字として解釈されるのを避けるためです。

$ grep -n -e '__init' audit.c audit_tree.c audit_watch.c auditfilter.c capability.
audit.c:937:static int __init audit_init(void)
audit.c:968:__initcall(audit_init);
audit.c:971:static int __init audit_enable(char *str)
audit_tree.c:943:static int __init audit_tree_init(void)
audit_tree.c:956:__initcall(audit_tree_init);
audit_watch.c:520:static int __init audit_watch_init(void)
auditfilter.c:157:int __init audit_register_class(int class, unsigned *list)
capability.c:30:static int __init file_caps_disable(char *str)

たくさんあるオプションのうち -n と -e をここで先に紹介します。

-n 検索結果に行番号を含める。
-e 検索パターンを明示的に指定する。検索パターンがマイナス記号でで始まる場合、grepからはそれがオプションなのかパターンなのか区別がつきません。-eと指定することで後続の引数がたとえマイナス記号で始まっていたとしてもPATTERNであると認識します。逆にマイナス記号で始まらないPATTERNについてはこの-eオプションを省略できます。

ファイルの指定

シェルのグロブ機能を使うとFILESの指定が簡単になります。現在のディレクトリ中の 全ての.cファイルを対象に検索する:

grep -e PATTERN *.c

現在のディレクトリ中の 全ての.cファイルおよびヘッダファイルを対象に検索する:

grep -e PATTERN *.[ch]

-rオプションを使うとFILES部分にディレクトリが含まれている場合、そのディレクトリ以下のファイルも全て検索の対象となります。

ソースコードツリーの巡回で説明するfindコマンドを使うと、より詳細にファイルを指定できます。例えばあるディレクトリ以下の全ての.cファイルを指定できます。

検索文字列の指定

次の正規表現メタ文字を使うと検索を詳細に制御できます。メタ文字の一部を紹介します。

^
行頭にマッチする。
$
行末にマッチする。
.
任意の一文字にマッチする。
[]
括弧の間の一文字とマッチする。
?
直前の表現に0回か1回マッチする。
*
直前の表現に0回以上マッチする。
\+
直前の表現に1回以上マッチする。
\b
単語の端の空文字列にマッチする。
\t
タブ文字にマッチする。

これらのメタ文字をただの文字として検索するには\を前置します。コードリーディングで良く使うのは^,[ \t],\bです。

コマンドラインオプション

-i
大文字小文字を無視して検索する。
-v
マッチしない行を表示します。
-B N
マッチした前N行も表示する。
-A N
マッチした後N行も表示する。
-l
マッチした行を表示せずファイル名だけを表示する。
-L
マッチしなかったファイル名だけを表示する。

具体的な利用例

検索結果に含まれるノイズ(マッチしてしまったが関心の無い検索結果)を下げるため、あるいは目的の箇所にうまくマッチさせるため、の姑息なテクニックを紹介します。

文字列リテラル

痕跡文字列として文字列リテラルを検索したくなることがあります。たとえGUIアプリケーションのメニューバーにある File メニュー の定義箇所を探すために

$ grep -n -e File *.c

としたくなります。ところがこのパターンでは、FileDialogTemporaryFileなど 文字列リテラル とは関係の無い名前にもマッチしてしまいます。こういった場合、ダブルクォート(")自体をパターンに含めてしまうと良いでしょう。

$ grep -n -e '"File"' *.c

あるいは

$ grep -n -e \"File\" *.c

とします。

シングルクォートで囲まれた文字定数を探す場合も同様にします。シングルクォート自体はシェルに解釈されてしまうので、

::
$ grep -n -e “‘c’”*.c

あるいは

$ grep -n -e \'c\' *.c

とします。

書式付き出力

痕跡文字列が書式付き出力経由で表示されていた場合、その痕跡文字列をそのまま検索しても、対応するコードポイントに辿りつけません。

次のような痕跡文字列を考えます。

3 errors are found.

これが実は、.. code-block:: c

printf(“%d errors are found.n”, count);

というコードを経て出力されている場合、

$ grep -n -e '3 errors are found\.' *.c

としても検索にひっかかりません。書式付き出力で展開されていそうな箇所には、.+を使って任意の文字列が適合するようパターンを調整します。

$ grep -n -e '.\+ errors are found\.' *.c

あるいは、いっそうのこと痕跡文字列を見て可変と考えられる部分をパターンから削ってしまっても良いかもしれません。

$ grep -n -e 'errors are found\.' *.c

関数定義

ある関数の定義をその名前から探そうとすると、関数の呼び出し箇所が多数マッチしてしまい、その中に定義箇所が埋もれてしまう、ということがあります。通常、目視でそれを選び出します。関数呼び出しの箇所には実引数が与えられている一方、関数定義箇所には、仮引数がその型ともに与えられているはずです。

$ grep -n -e 'foo' *.c
a.c:7:  x = foo(3);
a.c:20:         y = foo(a);
b.c:20  foo(int i)
b.c:35: if  (foo(a) < 0) {

ただし標準化される以前のc言語では、仮引数を列挙する箇所に型を記載しないことに注意して下さい

void
inittimeouts(val, sticky)
        register char *val;
        bool sticky;
{
        ...
/* Taken from sendmail-8.14.4/sendmail/readcf.c */

対象のソフトウェアが採用しているインデントスタイルによっては、正規表現メタ文字^を使って関数定義だけにマッチさせることができます。

関数定義において、修飾子と返り値の型の後に関数名を記述する、というスタイルがあります。このとき関数名が行頭に来ます。

const char *
dissector_handle_get_long_name(const dissector_handle_t handle)
{

このようなスタイルに従って記述されていると期待できる場合、^関数名というパターンで関数定義だけにマッチさせることができます。

$ grep '^dissector_handle_get_long_name' *.c

構造体定義

関数の定義と同様に構造体定義についても検索しようとすると、その構造体型を使って定義/宣言されている変数、その構造体型を引数あるいは返り値にとる関数の定義/宣言がマッチしてノイズにまみれます。

::
$ grep -nH -e vector s.cs.c:1:struct vector {s.c:6:struct vectors.c:7:vector_add(struct vector a, struct vector b)s.c:13:vector_innter_product(struct vector a, struct b)s.c:15: struct vector tmp;...

対象のソフトウェアが採用しているインデントスタイルによっては、構造体定義だけにマッチさせることができます。構造体定義において、フィールドの定義の開始を意味する{が構造体名の直後、改行を入れずに記載するというインデントスタイルのソースコードの場合、

struct vector {
        ...

次のパターンで構造体定義だけにマッチさせることができます。

$ grep 'struct vector \+{' *.c

構造体名の直後に改行を入れて{を記述するというインデントスタイルのソースコードの場合、

struct vector
{
        ...

次のパターンで構造体定義だけにマッチさせることができます。

$ grep 'vector foo$' *.c

ソースコードツリーの巡回

find

findコマンドを使うとあるディレクトリ以下にあるファイルのうち、指定した条件を満す各ファイルに対して指定したコマンドを実行できます。

読解中のソースコードツリーのトップレベルにカレントディレクトリがあるとします。典型的に使うのは次のようなコマンドラインです。

find . -type f -exec grep -nH -e PATTERN {} \;

この例ではカレントディレクトリ(.)以下の各ファイル(-type f)に対してgrepを実行します(-exec grep -nH -e PATTERN {}\;)。

-execオプションで適用したいコマンドラインを指定します。

  • \; でコマンドラインの末尾を表現します。
  • コマンド実行時に {} の部分が列挙したファイルと置き換えらます。

これを.cファイルとヘッダファイルだけに限定するには、-nameオプションを使います。

find . -type f -name '*.[ch]' -exec grep -nH -e PATTERN {} \;

-execを指定しない場合ファイルの名前を列挙できます。

find . -type f -name '*.[ch]'

xargs

  • 標準入力を読み込んであらたにコマンドラインを作成して、起動します。
  • 読み込んだ行(LINE)毎にコマンドラインを作成して、起動します。
  • xargsの引数をベースに、LINEを連結してコマンドラインを作ります。
find . type f -name '*.[ch]' | xargs grep -nH -e PATTERN

エディタからの利用

エディタの種類によっては、その内部からgrepを呼び出して検索結果を活用できるものがあります。

(近代的な)vi

:grep PATTERN FILE-OR-WILDCARD

として FILE-OR-WILDCARD 中から PATTERNを探すことができます。

:copen

検索結果の一覧を表示できます。一覧のなかで RETURN を押すと該当する行を、そのエディタのセッションの中で開くことができます。

Emacs

M-x grep
Run grep (like this): grep --color -nH -e PATTERN PATTERN FILE-OR-WILDCARD

として FILE-OR-WILDCARD 中から PATTERNを探すことができます。`*grep*`というバッファがオープンするので、その中で関心のある行を探し、RETURNを押すと該当する行を、そのエディタのセッションの中で開くことができます。