株式会社クリアコード > ククログ

ククログ


Rubyの拡張ライブラリにYARD用のドキュメントを書く方法

はじめに

YARDというRuby用のドキュメンテーションツールがあります。この記事ではCで書かれたRubyのライブラリにYARD用のドキュメントを書く方法を紹介します。

YARDはソースコード中にドキュメントを埋め込むタイプのドキュメンテーションツールです。ドキュメントはコメントとして書きます。ドキュメントに@タグ名という記法でメタデータを書けることが特徴*1です。YARDに添付されているyardocというコマンドを使うことで、ソースコード中に書いたドキュメントからHTMLのリファレンスマニュアルを作成することができます。

Ruby*2はライブラリをRubyでもCでも書けます*3。Cでライブラリを書くと、処理を高速化したり、既存のC/C++で書かれたライブラリをRubyから使えるようにできます。例えば、rroonga*4はC/C++で書かれた全文検索エンジンライブラリgroongaをRubyから使えるようにするライブラリです。

YARDはRubyで書いたライブラリもCで書いたライブラリもサポートしています*5。Cで書いたライブラリにYARD用のドキュメントを書くには少しコツがいります。ここでは、例をつけながら、Cで書いたライブラリにYARD用のドキュメントを書く方法を紹介します。具体的には次の5つについて説明します。

  • YARD用のドキュメントをどこに書いたらよいか
  • メソッドの説明を書く方法
  • メソッドの引数の説明を書く方法
  • メソッドの戻り値の説明を書く方法
  • メソッドに引数として渡すHashに指定できるキーの説明を書く方法

例の説明

まず、例として使うCのコードを示します。このコードは、rroongaで実際に使われているコードの一部です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    grn_ctx *context;
    grn_obj *database;
    int n_segments;
    VALUE options, rb_threshold;
    int threshold = 0;

    rb_scan_args(argc, argv, "01", &options);
    rb_grn_scan_options(options,
                        "threshold", &rb_threshold,
                        NULL);
    if (!NIL_P(rb_threshold)) {
        threshold = NUM2INT(rb_threshold);
    }

    rb_grn_database_deconstruct(SELF(self), &database, &context,
                                NULL, NULL, NULL, NULL);
    n_segments = grn_obj_defrag(context, database, threshold);
    rb_grn_context_check(context, self);

    return INT2NUM(n_segments);
}

void
Init_database ()
{
    VALUE mGrn;
    mGrn = rb_define_module("Groonga");
    rb_cGrnDatabase = rb_define_class_under(mGrn, "Database", rb_cObject);
    rb_define_method(rb_cGrnDatabase, "defrag", rb_grn_database_defrag, -1);
}

このコードで何をしているかを簡単に説明します。

このコードでは次の2つの関数を定義しています。

  • rb_grn_database_defrag()関数
  • Init_database()関数

1つ目のrb_grn_database_defrag()関数は、Groonga::Databaseオブジェクトのdefragメソッドの実体です。defragメソッドを呼ぶと、この関数が実行されます。

2つ目のInit_database()関数は、Groonga::Databaseオブジェクトのdefragメソッドとrb_grn_database_defrag()関数を結びつけています。初期化をしている関数です。

それでは、このCのコードにYARD用のドキュメントを書きながら、冒頭で挙げた次の5つについて説明します。

  • YARD用のドキュメントをどこに書いたらよいか
  • メソッドの説明を書く方法
  • メソッドの引数の説明を書く方法
  • メソッドの戻り値の説明を書く方法
  • メソッドに引数として渡すHashに指定できるキーの説明を書く方法

なお、この記事ではどこにどうタグ*6を書くかに焦点を当てているため、個別のタグに対する詳細な説明は省いています。タグの詳細についてはYARDのドキュメント(英語)を参照してください。

YARD用のドキュメントをどこに書いたらよいか

YARD用のドキュメントはコメント内に書きます。ドキュメント用のコメントはメソッドの実体となる関数の直前に書きます。関数の直前に書くと、関数定義とメソッドのドキュメントが結びつきます。結びつけられるとYARDが生成するHTMLのリファレンスマニュアルでは「View source」のリンク先に関数定義が表示されます。

例を示します。以下のコード内の「ここにYARD用のドキュメントを書く」と書かれた部分にドキュメントを書きます。こうすることにより、これから書くdefragメソッドのドキュメントとrb_grn_database_defrag()関数の定義が結びつきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
 * ここにYARD用のドキュメントを書く
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    grn_ctx *context;
    grn_obj *database;
    int n_segments;
    VALUE options, rb_threshold;
    int threshold = 0;

    rb_scan_args(argc, argv, "01", &options);
    rb_grn_scan_options(options,
                        "threshold", &rb_threshold,
                        NULL);
    if (!NIL_P(rb_threshold)) {
        threshold = NUM2INT(rb_threshold);
    }

    rb_grn_database_deconstruct(SELF(self), &database, &context,
                                NULL, NULL, NULL, NULL);
    n_segments = grn_obj_defrag(context, database, threshold);
    rb_grn_context_check(context, self);

    return INT2NUM(n_segments);
}

void
Init_database ()
{
    VALUE mGrn;
    mGrn = rb_define_module("Groonga");
    rb_cGrnDatabase = rb_define_class_under(mGrn, "Database", rb_cObject);
    rb_define_method(rb_cGrnDatabase, "defrag", rb_grn_database_defrag, -1);
}

なお、これ以降、例にはrb_grn_database_defrag()関数の定義部分とドキュメントのみを載せます。それ以外の部分はYARD用のドキュメントとは関係ないため省略します。

それではメソッドのドキュメントを書いていきます。

メソッドの説明を書く方法

メソッド定義の直前のコメントにタグ*7を使わずにドキュメントを書くと、YARDはその文章をメソッドの説明として扱います。メソッドの説明にはどのような処理をするメソッドかということを書きます。

defragメソッドの場合は以下のようになります。

1
2
3
4
5
6
7
8
/*
 * Defrags all variable size columns in the database.
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    /* ... */
}

このコードをexample.cというファイルに保存し、yardocコマンドを実行するとHTMLのリファレンスマニュアルを生成できます。

% yardoc example.c

リファレンスマニュアルには以下のようにコメントに書いたドキュメントがメソッドの説明として表示されています。

メソッドの説明を書いたリファレンスマニュアル

メソッドの説明は通常の文章として書きました。

メソッドの引数の説明を書く方法

メソッドの説明の後は、メソッドの引数についての説明を書きます。メソッドの引数の説明はタグを使って書きます。タグとはメタデータを指定するためのYARDの機能です。YARDではメソッドの引数をメタデータとして扱うため、統一感のある読みやすいリファレンスマニュアルを生成することができます*8

引数の説明には@paramタグを使います。@paramタグの書式は以下の通りです。

@param [引数のクラス] 引数名 引数の説明

では、実際に@paramタグで引数の説明を書きましょう。@paramタグを書く位置はメソッドの説明の下がよいでしょう。HTMLのリファレンスマニュアル上では順序は関係ありませんが、コード中のドキュメントを読む場合に読みやすくなります。この順序にすると、メソッド全体の説明を読み、次に引数の説明に入る、という順序になります。メソッドの全体像を把握してから細部を読めるのでドキュメントを理解しやすくなります。

1
2
3
4
5
6
7
8
9
10
/*
 * Defrags all variable size columns in the database.
 *
 * @param [Hash] options custom options.
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
   /* ... */
}

「引数の説明」の「custom options.」の最後には常に「.」を付けておいたほうがよいでしょう。説明が「custom options. Optional」と2文以上になった場合に最後の文だけ「.」がついていないと、もやっとするからです。

例のdefragメソッドのシグニチャー*9はRubyで書くと以下の通りです。

1
2
3
def defrag(options={})
  # ...
end

options引数はHashなので、@paramタグで[Hash]と書いてその情報を伝えています*10

ここまでで書いたドキュメントからyardocコマンドでHTMLのリファレンスマニュアルを生成すると次のようになります。

引数の説明を追加したリファレンスマニュアル

リファレンスマニュアルに引数の説明が追加されています。引数の説明を追加するために@paramタグを使いました。

メソッドの戻り値の説明を書く方法

戻り値の説明には@returnタグを使います。@returnタグの書式は以下の通りです。

@return [戻り値] 戻り値の説明

では、実際に@returnタグで戻り値の説明を書きましょう。@returnタグを書く位置は@paramタグの下がよいでしょう。入力を読んでから出力を確認する、という順序で読めます。

1
2
3
4
5
6
7
8
9
10
11
/*
 * Defrags all variable size columns in the database.
 *
 * @param [Hash] options custom options.
 * @return [Integer] the number of defraged segments.
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    /* ... */
}

例のdefragメソッドは戻り値として整数を返すので、@returnタグで[Integer]と書いてその情報を伝えています。

ここまでで書いたドキュメントからyardocコマンドでHTMLのリファレンスマニュアルを生成すると次のようになります。

戻り値の説明を追加したリファレンスマニュアル

リファレンスマニュアルに戻り値の説明が追加されています。戻り値の説明を追加するために@returnタグを使いました。

メソッドに引数として渡すHashに指定できるキーの説明を書く方法

最後に、Hashを引数として受け取るメソッドのドキュメントを書きます。Rubyでは、Pythonのキーワード引数相当のことを実現するために、引数をHashとして受け取り、メソッド内で必要な値を取り出します。このようなメソッドを使う側は、Hashに指定できるキーと、その値が何を意味するのかが気になります。これをドキュメントに書いておくことで、有用なドキュメントになります。

Hashにどんなキーを指定できるのかというドキュメントを書くには、次の3つのタグを使います。

  • @overloadタグ
  • @paramタグ
  • @optionタグ

まず、@overloadタグを使ってメソッドのシグニチャーを指定します。@overloadタグの書式は以下の通りです。

@overload メソッド名(メソッドの引数)

なお、@overloadタグは引数にHashを指定しない場合でも常に指定することをオススメします。Rubyで書かれたメソッドは@overloadタグを書かなくても引数名などの引数の情報がつきますが、Cで書かれたメソッドには@overloadタグを書かないと引数の情報がつかないからです*11

では、実際に@overloadタグでメソッドのシグニチャーを書きましょう。@overloadタグを書く位置は@paramタグの上がよいでしょう。まず、引数全体を確認してから個々の引数を確認する、という順序で読めます。

1
2
3
4
5
6
7
8
9
10
11
12
/*
 * Defrags all variable size columns in the database.
 *
 * @overload defrag(options={})
 * @param [Hash] options custom options.
 * @return [Integer] the number of defraged segments.
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    /* ... */
}

ここまでで書いたドキュメントからyardocコマンドでHTMLのリファレンスマニュアルを生成すると次のようになります。

シグニチャーの説明を追加したリファレンスマニュアル

リファレンスマニュアル内の、「Instance Method Summary」と「Instance Method Details」にあるdefragメソッド名のところに、「(options = {})」が追加されています。

次に、@paramタグでHashで指定するオプション全体についての説明を書きます。@paramタグの説明では「Hashでオプションを渡すことができる」ということを説明するのがよいでしょう。

@paramタグの書き方で注意するポイントは、必ず@overloadタグよりも下に書き、さらにその@overloadタグよりもインデントして書かなければいけないという点です。インデントして書くことで、YARDがその@paramタグは@overloadタグで書いたシグニチャーに対応していると認識します。

実は@overloadタグを複数指定することにより複数のシグニチャーを指定することができます。もし、@paramタグが@overloadタグと同じインデントレベルにある場合は「すべての@overloadタグで共有される@paramタグ」と認識されます。多くの場合はそれぞれのシグニチャー毎に引数の説明は異なるため、@overloadタグ毎に@paramタグが認識される書き方の方が適切です。

例ではすでに@paramタグが書かれていますが、@overloadタグと同じインデントレベルになっています。そのため、@overloadタグよりもインデントして@paramタグを書くように修正します。同様に@returnタグもインデントします。インデントする理由は@paramタグと同じです。

1
2
3
4
5
6
7
8
9
10
11
12
/*
 * Defrags all variable size columns in the database.
 *
 * @overload defrag(options={})
 *   @param [Hash] options custom options.
 *   @return [Integer] the number of defraged segments.
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    /* ... */
}

これで、@paramタグと@returnタグが@overloadタグで書いたシグニチャーに対応しているとYARDが認識するようになります。

ここまでで書いたドキュメントからyardocコマンドでHTMLのリファレンスマニュアルを生成すると次のようになります。

@paramタグと@returnタグが@overloadタグで書いたシグニチャーに対応したリファレンスマニュアル

@returnタグに書いた戻り値の説明が、メソッドの説明の後ろに追加されています。@paramタグに書いた引数の説明は追加されていませんが、これは、@overloadタグが1つしかないため、1つの@overloadタグで使われているかすべての@overloadタグで共有されているかの見分けがつかないためです。*12 @overloadタグで書いたシグニチャーに@paramタグと@returnタグが対応しているとYARDに認識させるために、@paramタグと@returnタグをインデントしました。

最後に、@optionタグでHashに指定できるキーとその説明について書きます。@optionタグの書式は以下の通りです。

@option Hash引数の名前 Hashのキー名 (デフォルト値) 値の説明

ここでの「デフォルト値」とは、Hashにキーを指定しなかったときに、そのキーに対応する値として使用される値のことです。

@optionタグは@paramタグの下に同じインデントで書きます。

では、実際に@optionタグを書きましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
 * Defrags all variable size columns in the database.
 *
 * @overload defrag(options={})
 *   @param [Hash] options custom options.
 *   @option options [Integer] :threshold (0) the threshold to
 *     determine whether a segment is defraged. Available
 *     values are -4..22. -4 means all segments are defraged.
 *     22 means no segment is defraged.
 *   @return [Integer] the number of defraged segments
 */
static VALUE
rb_grn_database_defrag (int argc, VALUE *argv, VALUE self)
{
    /* ... */
}

ここまでで書いたドキュメントからyardocコマンドでHTMLのリファレンスマニュアルを生成すると次のようになります。

Hashのキーの説明を追加したリファレンスマニュアル

オプションについての説明が追加され、引数のHashに指定できるキーが:thresholdであることと、:thresholdに対応する値は整数を指定することと、指定しなかったときには0が使われることがわかります。Hashでオプションを指定する場合のドキュメントには@overloadタグ、@paramタグ、@optionタグを使いました。

まとめ

Cで書いたライブラリにYARD用のドキュメントを書く方法を説明しました。ポイントは@overloadタグを使うことです。Rubyで書いたライブラリの場合は@overloadタグは必須ではありませんが、Cで書いたライブラリの場合は必須と言ってよいでしょう。YARD用のドキュメントを書くことで、ユーザーにとって有用なドキュメントを書いてみてはいかがでしょうか。よいソフトウェアを書くためにドキュメントを書くことが役に立つこともありますよ。

*1 RDocでは:XXX:という記法でディレクティブを指定できます。ディレクティブはメタデータを指定するというよりは出力を制御するものです。ただし、ディレクティブの中には:category:などメタデータを指定するものもあります。

*2 CRubyやMRIと呼ばれている実装。

*3 Cで書かれたRubyのライブラリを拡張ライブラリと呼びます。

*4 rubyforge.orgからranguba.orgに移動しました。

*5 RDocもRubyとCを両方サポートしています。

*6 後述。

*7 もっと後述。

*8 RDocはドキュメントを書く人それぞれが引数の説明っぽくドキュメントを書くという方式で、RDoc自身は引数の説明を特別扱いしません。そのため、書く人により表示のされ方は様々です。

*9 メソッドの名前、引数、戻り値に関する情報のこと。Rubyのメソッド定義の構文には戻り値に関する情報は含まれない。

*10 options引数が省略可能という情報は後で指定します。

*11 Rubyで書かれたメソッドの場合はメソッド定義から引数の情報を抽出しているが、Cではそもそも引数の情報が書かれていないため。

*12 もう少し言うと、もう1つ@overloadタグがあると、その@overloadタグの下には現在ある@paramタグの説明が表示されないので見分けがつきます。

タグ: Ruby
2012-10-02

シェルスクリプトで「ビルドスクリプト」を作る時に便利なテクニック

プログラムの種類によっては、そのまま実行できるものと、実行できるようにするために「ビルド」が必要なものとがあります。Cなどのコンパイルが必要な言語で書かれたプログラムは当然ビルドが必要ですし、コンパイルが不要な言語であっても、インストーラパッケージを作るというビルド作業が必要な場合はあります。

ビルド作業の自動化のためのツールとしてmakeなどがありますが、そこまで本格的な事をやる必要がない場合は、シェルスクリプトで「ビルドスクリプト」を作るのが手軽でおすすめです。この記事では、そのような場合に役立つシェルスクリプトのテクニックを4つご紹介します。

  • エラーの気付きやすさとデバッグのしやすさを高める
  • メッセージに色を付ける
  • シェル関数をライブラリにする
  • 一時的に作業ディレクトリの中に入る

エラーの気付きやすさとデバッグのしやすさを高める

はじめに紹介するテクニックは問題が発生した時に気づきやすくするためのテクニックです。

コマンドの実行に失敗したらその場で終了する

シェルスクリプトを使って簡易的なビルドスクリプトを作る時には、「途中のどこかで失敗したらすべての処理を中断する」ということをやりたくなるものです。そうしないと、途中でエラーが起こっているのにビルド処理が最後まで走ってしまい、「見た目だけはきちんとしているけれども実は壊れている」という中途半端な生成物ができてしまうからです。

中途半端な生成物になってしまっているかどうかは、実際にそれをインストールする・動かすなどの操作をしてみないと分かりません。それに対して、そもそもエラー発生時にビルド処理が中断されていれば、「最終生成物ができていない」ということ自体から容易にトラブルの発生に気がつけます。

エラー発生時に全体の処理を中断する最も簡単なやり方は、シェルの内部コマンドの set を使う方法です。bashやzshなどのsh互換のシェルでは、 set -e とすると、途中のコマンドのどれか1つでも失敗した*1時点で自動的にシェルスクリプトの実行が終了するようになります。これは、個々のコマンド列について command || exit 1 などと書いても同じような効果を得られます。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
# set-e-test.sh

echo "Run 'cp /etc/hosts /root/' without 'set -e'"
cp /etc/hosts /root/

set -e
echo "Run 'cp /etc/hosts /root/' with 'set -e'"
cp /etc/hosts /root/

echo "Finished"

実行してみましょう。

% chmod +x set-e-test.sh
% ./set-e-test.sh
Run 'cp /etc/hosts /root/' without 'set -e'
cp: cannot stat `/root/hosts': Permission denied
Run 'cp /etc/hosts /root/' with 'set -e'
cp: cannot stat `/root/hosts': Permission denied

最初のcpは失敗しても実行が継続しますが、2つめのcpは失敗したらそこで終了しています。これは「Finished」が出力されていないことからわかります。

失敗したコマンドが何だったのかを知る

ただ、この方法には1つ問題があります。それは、「どのコマンドの実行に失敗したのか」が分からないという事です。

通常、シェルスクリプトでは実行したコマンドの実行結果は出力されますが、どのようなコマンドを実行したのかまでは出力されません。単に「Permission denied.」とだけ出ても、何が原因でそのエラーが発生したのかが分からないと、デバッグは非常に困難です*2

シェルの内部コマンドの set を使うと、この問題を解消できます。set -x*3とすると、シェルスクリプトの中で実行したコマンド列そのものが標準エラー出力に出ます。

1
2
3
4
5
6
7
8
9
#!/bin/sh
# set-x-test.sh

echo "Run 'cp /etc/hosts /root/' without 'set -x'"
cp /etc/hosts /root/

set -x
echo "Run 'cp /etc/hosts /root/' with 'set -x'"
cp /etc/hosts /root/

実行してみましょう。

% chmod +x set-x-test.sh
% ./set-x-test.sh
Run 'cp /etc/hosts /root/' without 'set -x'
cp: cannot stat `/root/hosts': Permission denied
+ echo Run 'cp /etc/hosts /root/' with 'set -x'
Run 'cp /etc/hosts /root/' with 'set -x'
+ cp /etc/hosts /root/
cp: cannot stat `/root/hosts': Permission denied

+ 」から始まる行がset -xが出力している行です。コマンドを実行する前にコマンド列を出力しています。

もっとスマートに!

ということで、set -exとすると「コマンドの実行に失敗したらその場で終了する」と「どのコマンドで失敗したかを知る」という2つのことを実現できるのですが、これには1つデメリットがあります。set -xすると、echoのように失敗することがないコマンドなども含めてすべてのコマンド列が出力されるようになるため、出てくる情報が多すぎるのです。多すぎる出力は重要な情報を埋もれさせてしまうので、できれば慎みたいものです。

要するに、「コマンドの実行に失敗したらその場で終了する」と「どのようなコマンド列が実行されたかを表示する」という2つのことを、全部のコマンド列でやるのではなく、特定のコマンド列*4に対してだけやれるようにしたい、ということになります。setコマンドの影響はそのシェルスクリプト全体に及んでしまうので、個々のコマンド列のレベルで同じような目的を達成するには、何か別の方法を使わなくてはなりません。

前述しましたが、command || exit 1とすると、commandが失敗した時にだけexit 1が実行されてそこでスクリプトの実行が中断される、という挙動にできます。シェルスクリプト全体ではなく、個々のコマンド毎に挙動を変更できるのでやりたいことに近くなります。ただ、これだけだと「どのようなコマンド列の実行に失敗したのか」を知ることができません。両方の望みを同時に叶えるためには、シェル関数を定義する必要があります。

シェル関数にすることには、もう1つメリットがあります。それは、そのコマンド列が「失敗しても継続するものなのかどうか」を判別しやすくなるという点です。コマンド列の最後に|| exit 1を付けた場合、行末を見ないと、コマンドが失敗したらどうなるのかということを判別できません。行の長さはまちまちなので、これでは余計に判別が困難です。それに対して、シェル関数にしておくと、関数名 commandという風に行頭に情報が表れるため、ざっとスクリプト全体を眺めた時に、どれが失敗しても大丈夫でどれがそうでないのかという事が分かりやすくなります。

例えば、関数名CHECKとすると以下のような見た目になります。

1
2
3
4
5
6
7
# 行末を見て判断
mkdir /usr/local/bin || exit 1 # 失敗してはダメ
mkdir /usr/local/bin           # 失敗してもよい

# 行頭を見て判断
CHECK mkdir /usr/local/bin     # 失敗してはダメ
mkdir /usr/local/bin           # 失敗してもよい
シェル関数の定義例

前述のような事情から、run commandと書くと「コマンドの実行に失敗したらその場で終了する」と「どのようなコマンド列が実行されたかを表示する」が実現されるようなシェル関数runを定義して利用しています。ここでのrunが前述のCHECK相当の関数です。

1
2
3
4
5
6
7
8
9
10
11
# run.sh
run() {
  "$@"
  result=$?
  if [ $result -ne 0 ]
  then
    echo "Failed: $@ [$PWD]" >&2
    exit $result
  fi
  return 0
}

このように使います。

1
2
3
4
5
6
7
#!/bin/bash
# run-sample.sh
source ./run.sh

run echo "Start"
run cp /etc/hosts /root
run echo "Finished"

実行するとこうなります。

% chmod +x run-sample.sh
% ./run-sample.sh
Start
cp: cannot stat `/root/hosts': Permission denied
Failed: cp /etc/hosts /root [/tmp]

コマンドの実行に失敗したときだけコマンド列を標準エラー出力に出しています。

それでは、run関数の中身を順番に説明します。

1
"$@"

$@は、シェルスクリプト自体に渡されたすべての引数を参照する変数です。シェル関数の中で参照した場合は、そのシェル関数に渡された引数を参照することになります。ここでは渡されたコマンド列そのものを実行しています。runには「実行したいコマンド」も含めた内容が渡されますので、$@はそのままコマンド列として実行することができます。単に$@とせずに、ダブルクォートで囲って"$@"としていることに注意してください。こうしないとスペース入りの引数が複数の引数に分割されてしまいます。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# at-with-double-quote-test.sh

with_double_quote() {
  "$@"
}

without_double_quote() {
  $@
}

with_double_quote    ruby -e 'p(ARGV)' a "b c" d
without_double_quote ruby -e 'p(ARGV)' a "b c" d

このスクリプトを実行すると違いがわかります。

% chmod +x at-with-double-quote-test.sh
% ./at-with-double-quote-test.sh
["a", "b c", "d"]
["a", "b", "c", "d"]

ダブルクォートで囲んだ"$@"だと空白入りの引数"b c"がそのまま1つの引数として渡っています。一方、ダブルクォートで囲まない$@だと空白入りの引数が分割されています。

$@と同じような用途の変数で$*もありますが、引数をそのまま実行するという用途には"$@"が適切です*5

1
result=$?

$?は、直前に実行したコマンド列の終了時のステータスコードを示す変数です。後でもう一度使うので、ここでは一旦別の名前の変数を定義しています。

1
2
3
4
if [ $result -ne 0 ]
then
  # ... # ステータスコードが0以外であった場合の処理
fi

ここでは、ステータスコードが0かどうかを判別しています。0であればコマンド列の実行に成功しており、それ以外であれば失敗です。[ A -ne B ]は整数型の値同士の比較で、両者が等しくなければ(not equal)結果が真になります。

1
2
echo "Failed: $@ [$PWD]" >&2
exit $result

ステータスコードで失敗と示されている場合には、実行したコマンド列とカレントディレクトリの位置*6を出力します。その後、exitでスクリプトの実行を中断しています。この時、exitの引数に先の「本当に実行したかったコマンド列の終了時のステータスコード」を渡すことによって、このスクリプト自体が他のスクリプトから呼ばれている場合であっても、エラーの発生が呼び出し元のスクリプトに伝搬するようになります。

1
return 0

コマンド列の実行に成功していた場合はステータスコード0を返してシェル関数を抜けます。

ここで紹介したようなrun関数を使えば、コマンドの失敗を見つけやすくなるはずです。

メッセージに色を付ける

シェルスクリプトでは、出力を色分けするのもおすすめです。すべての出力が同じ色だと、コマンドの引数と結果とを見間違えたり、そのような見間違いを警戒して出力を読むスピードが低下したり、といった形でデバッグのしやすさが低下してしまいます。

先の「失敗したコマンド列を出力する」という話にも言えることですが、バグは発生しないものだ*7という前提で考えてしまうと、エラー発生時の対策をなおざりにしてしまいがちです。そのような場合に備えた対策がなされていないと、デバッグに時間がかかって、余計に消耗してしまいます。そうではなく、人はミスをするものだ・バグは発生するものだ、という前提に立ってデバッグのしやすさに気を遣うようにすると、余計なストレスに悩まされずに済みます。

以下は、前述のシェル関数runの例について、どの部分が実際に実行されたコマンド列なのか、どの部分が作業ディレクトリのパスなのか、といったことを色分けして表示するようにした例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# run-with-color.sh

red=31
yellow=33
cyan=36

colored() {
  color=$1
  shift
  echo -e "\033[1;${color}m$@\033[0m"
}

run() {
  "$@"
  result=$?

  if [ $result -ne 0 ]
  then
    echo -n $(colored $red "Failed: ")
    echo -n $(colored $cyan "$@")
    echo $(colored $yellow " [$PWD]")
    exit $result
  fi

  return 0
}

このように使います。

1
2
3
4
5
6
7
#!/bin/bash
# run-with-color-sample.sh
source ./run-with-color.sh

run echo "Start"
run cp /etc/hosts /root
run echo "Finished"

実行するとこうなります。

% chmod +x run-with-color-sample.sh
% ./run-with-color-sample.sh
Start
cp: cannot stat `/root/hosts': Permission denied
Failed: cp /etc/hosts /root [/tmp]

色つき文字列の出力には、エスケープシーケンスを使用します。エスケープシーケンスを使うと、カラーコードを指定して、echoで出力される文字列に任意の色を付けることができます*8

なお、上記のスクリプトはbash用スクリプトとして書かれていますが、bashでechoコマンドを実行した場合、初期状態ではエスケープシーケンスが無視されてしまいます*9。ですので、エスケープシーケンスを明示的に有効化するために、coloredの中で明示的にecho -eとしています。dash用スクリプトとして実行する*10場合は、dashのechoでは初期状態でエスケープシーケンスが有効なため、-eオプションを指定する必要はありません。

シェル関数をライブラリにする

元のシェル関数runは各シェルスクリプトにコピー&ペーストして使ってもまだ大丈夫な規模でしたが、色づけ表示するようにした例では、カラーコードの定義行なども含めるとそれなりの規模になってしまっています。このような場合は、コピー&ペーストするのではなく、ライブラリとして独立したファイルにしておいた方が便利でしょう。

シェル関数を定義する内容のシェルスクリプトをrun.shのような名前で設置しておき、他のスクリプトからsource ./run.shなどとしてインポートすると、他のスクリプトの中でもrunを利用できるようになります。これは、実はrun関数の実行例の説明のときにすでに使っていたテクニックです。

一時的に作業ディレクトリの中に入る

ビルド対象が複数のモジュールに別れている時には、サブディレクトリに置いてある各モジュールをそれぞれビルドして、最後に上位のディレクトリで全体をビルドする、という順番で処理を行うことになります。

この時、うっかり以下のようなスクリプトを書いてしまうと、困った事になる場合があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
main() {
  build_module module1
  build_module module2
}

build_module() {
  cd $1 && \
    ./build.sh && \
    cp $1.zip ../dist/ && \
    cd ..
}

main

cdで各モジュールのディレクトリに移動してそのモジュールごとのビルドスクリプトを実行し、できあがったファイルを最終出力のディレクトリにコピーした後、上位のディレクトリに戻るというスクリプトです。

このスクリプトの一番大きな問題点は、「モジュールのビルドに失敗した時に、カレントディレクトリが移動したままになってしまう」ということです。上記の例であれば、もしmodule1のビルドスクリプトの実行中にエラーが発生した場合、カレントディレクトリはmodule1のディレクトリのままで関数build_moduleを抜けてしまい、次のbuild_module module2に処理が進んでしまいます。そのため、ディレクトリが見つからなかったり、ファイルが見つからなかったり、期待していた内容と異なる内容のファイルが最終出力のディレクトリにコピーされてしまったり、といった予想外のトラブルが発生してしまう恐れがあります。

このような場合には、ディレクトリの移動を伴う一連のコマンドを(...)で括ると安全です。

1
2
3
4
5
build_module() {
  (cd $1 && \
    ./build.sh && \
    cp $1.zip ../dist/)
}

(...)の中には通常通りコマンド列を記述できますし、;を区切りとして使えば複数のコマンドを記述することもできます。この時ありがたいことに、(...)の内側でどれだけカレントディレクトリを移動しても、) を抜けた後は(に入る前のカレントディレクトリに強制的に戻されるという特性があります。この特性を利用すれば、複数のディレクトリに別れたサブモジュール群のビルド工程を含むビルドも安心して実行できます*11

まとめ

以上、シェルスクリプトを使って簡易的なビルドスクリプトを記述する際に有用なテクニックを4つご紹介しました。

定型的な作業は、このようにして極力自動化しておくことが大事です。パッケージング1つとっても、単にファイルをZIPで固めるだけだから……といった感じで毎回手作業でやっていると、人為的なミスが入り込む余地がありますし、他の人に引き継ぐのも大変です*12。また、そのうち作業が億劫になってきて、リリース自体が滞ってしまうことすらあります。

また、ミスを犯さないよう慎重になることは重要ですが、それとフットワークの重さとは別の話です。余計なことに多くの時間を取られるせいでミスが見過ごされてしまっては、元も子もありません。

ソフトウェアやサービスをリリースすることを、英語圏ではshipと表現することがあります*13。皆さんも、この記事で紹介したテクニックなどを駆使してshippabilityを高く保ち、プロジェクトを常にshippableな状態にしておくことを心がけてください。

*1 終了時のステータスコードが0以外になった

*2 シェルスクリプト自体のエラーであれば行番号が出ますが、外部コマンドではその限りではありませんし、コマンドによっては何もメッセージを出さずに終了時のステータスコードだけで異常を知らせるものもあります。

*3 上記の -eと組み合わせるのであればset -ex

*4 例えば、失敗する可能性がある、失敗すると影響が大きいコマンド列

*5 'bash(1)'の「特殊パラメータ」の「*」と「@」の説明を読むと違いがわかります。

*6 デバッグ性を高めるため。

*7 自分はミスをしない

*8 カラーコードに応じて文字の色を変えるのはシェルではなくGNOME端末などの端末側の機能です

*9 エスケープ文字も含めてそのまま出力されます。

*10 例えばUbuntuでは#!/bin/shとするとdashが使われます。

*11 サブシェルで実行しているためです。この機能について興味がある人はサブシェルで調べてみてください。

*12 他の人でなくても、自分自身が過去にやっていた作業をしばらくぶりにまたやるようになったという場合にも、手順が複雑だと、また覚え直すのに苦労することになります

*13 出荷するという意味

2012-10-11

手元のgemのコードを簡単検索

最近のプログラミング言語はパッケージ管理システムを持っていることがほとんどです。PerlにはCPAN*1がありますし、PythonにはPyPi*2がありますし、RubyにはRubyGems*3がありますし、Node.jsにはnpm*4があります。パッケージ管理システムがあると簡単にライブラリやツールをインストールできるので、手元にたくさんのコードが集まります。そんな手元のコードを簡単に検索できるようにする方法を紹介します。ただし、ここで紹介するのはRubyGemsでインストールしたパッケージのコードを簡単に検索できるようにする方法だけです。他のパッケージ管理システムについては触れません。

ライブラリを使っていて、期待した動作をしないときはどうしますか?まず、ドキュメントを確認することでしょう。ドキュメントを読んでも解決しないときはWebで検索したり、実際にコードを読んでみることでしょう。ここで紹介する方法を使えばコードを読むまでのコストが下がるので今までより気軽にコードを確認できるようになるはずです。

gemを簡単に検索できるようにする方法

gem-milkodeというgemをインストールするだけです。

% gem install gem-milkode

後はいつも通りgem installでgemをインストールしてください。

% gem install rails

検索したくなったらmilk webを実行してください。Webブラウザーに検索画面が表示されるので、そこからサクサク検索できます。

% milk web

gem installじゃなくてbundle installを使っているんだけど…

最近はgem installで個々のgemをインストールするのではなく、Bundlerでgemをインストールすることが多くなりましたね。そんなあなたはbundle-milkodeをインストールしてください。

% gem install bundle-milkode

後はbundle installbundle updateの代わりにbundle-milkodeを使うだけです。

% bundle-milkode install
% bundle-milkode update

これでBundlerでインストールしたgemも簡単に検索できるようになります。

検索したくなったらmilk webを実行してください。Webブラウザーに検索画面が表示されるので、そこからサクサク検索できます。

% milk web

仕組み

gem-milkodeもbundle-milkodeもgemをインストールする時にMilkodeにgemのコードを登録しているだけです。Milkodeは行指向のソースコード検索システムです。コードを登録すればあとはMilkodeがいい感じにやってくれます。gem-milkodeとbundle-milkodeは「Milkodeにコードを登録する」という少し面倒な作業をこっそりやってくれるだけです。でも、それが便利なんです。

さて、それでは、gem-milkodeとbundle-milkodeはどのような仕組みで動いているのでしょうか。

bundle-milkode

bundle-milkodebundleのラッパーみたいなものなので特別に何かをしているわけではありません。bundleの機能を実行した後に新しくインストールされたgemをMilkodeに登録しているだけです。短いコードなので、コードを読むとすぐにわかります。

コード: bundle-milkode

gem-milkode

gem-milkodeはRubyGemsのプラグイン機能を使っています*5。プラグイン機能の使い方は簡単です。gemにlib/rubygems_plugin.rb*6というファイルを含めるだけです。後はRubyGemsが勝手に読み込んでくれます。

注意する点は、インストールされているすべてのgemのlib/rubygems_plugin.rbが読み込まれるということです。例えば、gem-milkode-1.0.1とgem-milkode-1.0.2がインストールされているときは、1.0.1のlib/rubygems_plugin.rbも1.0.2のlib/rubygems_plugin.rbも読み込まれます。そのため、プラグインが提供する同じ機能が何度も実行される可能性があります。

これを回避するために、「複数のバージョンをインストールしないように!」と呼びかける方法と、複数のバージョンがインストールされていても最新のものだけ実行する方法があります。gem-milkode 1.0.2までは前者でしたが、1.0.3からは後者になっています*7。gem-milkodeも短いコードなので、やり方はコードを見てください。

コード: rubygems_plugin.rb

まとめ

インストールしたgemを自動でMilkodeに登録する小さなツールを紹介しました。このツールを使うことで簡単にgemのコードを検索できるようになります。gemのコードを読む敷居が下がるので、たくさんコードを読んでみてください。

おそらく、RubyGems以外のパッケージ管理システムにもプラグインのような機能があるはずなので、ここで紹介したツールと同じようなツールを作ればNode.jsやPythonでも簡単にコードを検索できるようになりますね。

*1 フロントエンドはいくつかあるみたい。

*2 パッケージ管理システムというかパッケージ配布サイト。フロントエンドはいくつかある。

*3 これはパッケージ配布サイトもパッケージ管理ツールも提供。

*4 これもサイトもツールも提供。

*5 リンク先を見ればわかる通り、すでにいろんなプラグインがあります。エディターでgemのファイルを開くコマンドを提供するプラグインがいくつもあることが興味深いですね。やはり、みんなコードを読みたくなるようです。gemのソースをgit cloneするプラグインもあります。

*6 lib/以下じゃなくても$LOAD_PATHが通っている場所であればどこでもよいです。説明が面倒になるので、ここではlib/に置くということで進めます。

*7 つまり、gem-milkode 1.0.2以下はさっさとアンインストールしてgem-milkode 1.0.3以降を使ってください、ということです。

タグ: Ruby
2012-10-18

シェルスクリプトとMakefileの使い分け

先日紹介したシェルスクリプトで「ビルドスクリプト」を作る時に便利なテクニックへのコメントとして「なぜMakefileでやらないのか」「Makefileの方がいいのではないか」といったものがありました。確かにmakeはメジャーなビルドツールなので、そのような疑問が出てくるのも当然でしょう。

なぜシェルスクリプトなのかということの理由はいくつかあります。

1つは、先のエントリの題材としたスクリプトが元々はWindows用のバッチファイルをLinuxのシェルスクリプトに移植したものだったからという理由です。Windowsのバッチファイルのベタ移植として作成したシェルスクリプトを継続的にメンテナンスしてきた間の改良の結果として、いくつかのテクニックが盛り込まれるようになったため、そのテクニックにスポットを当てて紹介しようというのが、先のエントリの発端でした。

もう1つは、シェルスクリプトは「シェルのコマンドを列挙したビルドスクリプト」を作る上では最適な手段だからという理由です。「シェルのコマンドを列挙したビルドスクリプト」の場合は、必ずしもMakefileだけで完結させるのが最良とはいえません。以下、先のエントリの補足も兼ねて、こちらの点について詳しく説明します。

Makefileとは

シェルスクリプトとMakefileの違いについて述べる前に、まずMakefileについて簡単に説明します。

アプリケーションを使える状態にするには、実行用のファイルをビルドする必要があったり、あるファイルをビルドするにあたって依存する別のファイルを先にビルドしておかなくてはならなかったりと、色々と気をつかわなくてはならないことがあります。そのため、個々のファイルのビルド手順と、ファイル同士の依存関係を整理して、「依存するすべてのファイルを自動的にビルドする」「一部の構成ファイルに変更があった場合はそれに依存するファイルだけを再ビルドする」といった事を自動的に行えるようにする仕組みが必要とされてきました。その代表的なツールがmakeであり、make用の設定ファイルがMakefileです。

Makefileの最も単純な使い方では、以下のような書式でファイル同士の依存関係と各ファイルのビルド方法を記述します。

1
2
3
4
ビルド対象のファイル: 依存するファイル
        ビルドするためのコマンド列1
        ビルドするためのコマンド列2
        ...

コマンド列を記述する行は、行頭にタブ文字を置いてインデントします。

例えば「ZIP形式のアーカイブであるmyaddon.jarというファイルをビルドする必要があり、content、locale、skinという3つのディレクトリが必要である」という時には、以下のように書きます*1

1
2
myaddon.jar: content locale skin
        zip -q -r -9 myaddon.jar content locale skin

また、Makefileの中では以下のようにして「マクロ」を定義しておくこともできます。

1
PACKAGE_NAME = myaddon

マクロとは、特定の文字列に名前を付けて、同じ事を何度も書かなくてもその名前を書くだけで参照できるようにする仕組みです。定義したマクロは、以下のようにして自由に参照できます。

1
2
3
4
5
JAR_TARGET_FILES = content locale skin
PACKAGE_NAME = myaddon

$(PACKAGE_NAME).jar: $(JAR_TARGET_FILES)
        zip -q -r -9 $(PACKAGE_NAME).jar $(JAR_TARGET_FILES)

以上を踏まえて、Firefox用のアドオンのインストーラパッケージを作成するMakefileの具体例を以下に示します。ここでは、PACKAGE_NAMEPACKAGE_VERSIONJAR_TARGET_FILES、およびXPI_TARGET_FILESの4つのマクロと、xpi、および$(PACKAGE_NAME).jarの2つのビルド対象ファイル(これを「ターゲット」と呼びます)を記述しています。ビルド手順としてのコマンド列は、ファイルをコピーしたり、ディレクトリを用意したりした上で、ZIP形式で圧縮するだけという単純なものです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PACKAGE_NAME = myaddon
JAR_TARGET_FILES = \
        content \
        locale \
        skin
XPI_TARGET_FILES = \
        install.rdf \
        components/*.js \
        components/*.xpt \
        chrome \
        defaults \
        modules \
        chrome.manifest

xpi: $(PACKAGE_NAME).jar
        rm -rf chrome
        mkdir -p chrome
        cp $(PACKAGE_NAME).jar chrome/
        rm -f $(PACKAGE_NAME).xpi
        zip -q -r -9 $(PACKAGE_NAME).xpi $(XPI_TARGET_FILES)

$(PACKAGE_NAME).jar: $(JAR_TARGET_FILES)
        rm -f $(PACKAGE_NAME).jar
        zip -q -r -9 $(PACKAGE_NAME).jar $(JAR_TARGET_FILES)

ターゲット「xpi」は、配布用のパッケージを作ることが目的のコマンドに名前を付けた便宜的なターゲットです。実際のファイル名とは結びついていません。

シェルスクリプトとMakefileの違い

シェルスクリプトはシェルで実行するコマンドを列挙したものです。対するMakefileは、ターゲットごとにビルド手順のコマンドを列挙したものです。シェルのコマンドを列挙するものであるという点で両者はよく似ていますが、いくつか違うところもあります。

シェルの特殊な機能を使う際に注意が必要

Makefileの場合、個々のコマンド列は直接シェルによって実行されるのではなく、一旦makeによって解釈されるという点に気をつけなくてはいけません。

上記のMakefileの例において、$(PACKAGE_NAME)と書いている点に注目して下さい。シェルで変数を参照する場合は${変数名}と書きます。$(...)はコマンド置換の書き方です。前述した通り、このコマンド列はまずmakeによって解釈され、その上でシェルのコマンドとして実行されます。よって、$(PACKAGE_NAME)という記述はシェルのコマンド置換ではなくMakefileでのマクロの参照として処理され、シェルにはその結果が渡されます。

では、シェルのコマンド置換を使いたい場合はどうなるでしょうか。例えば、バージョン番号をversion.txtというファイルで管理していて、そのファイルの内のにあるバージョン番号を生成するファイル名の一部に使いたいという場合です。単純に考えると、例えば以下のようになるでしょう。

1
2
3
4
5
6
xpi: $(PACKAGE_NAME).jar
        rm -rf chrome
        mkdir -p chrome
        cp $(PACKAGE_NAME).jar chrome/
        rm -f $(PACKAGE_NAME)-*.xpi # ファイル名が不定になるため
        zip -q -r -9 $(PACKAGE_NAME)-$(cat version.txt).xpi $(XPI_TARGET_FILES)

ところが、これでは期待通りの結果が得られません。コマンド置換のつもりで書いた箇所が、makeによって先にマクロとして展開されてしまうため、シェルに渡されるコマンド列は以下のようになってしまうからです*2

zip -q -r -9 myaddon-.xpi content locale skin

こうならないようにするためには、makeにマクロの参照として認識させたくない$をエスケープする必要があります。$$と書くと、マクロ参照の指示ではない文字として$を記述できます。

1
        zip -q -r -9 $(PACKAGE_NAME)-$$(cat version.txt).xpi $(XPI_TARGET_FILES)

makeによってマクロが展開された後、実際にシェルに渡されるコマンド列は以下のようになります。

zip -q -r -9 myaddon-$(cat version.txt).xpi content locale skin

これでやっと期待通りの結果を得られます。

なお、ここではコマンド置換の例を示しましたが、forでループする処理を書く必要がある場合など通常のシェル変数を使う時にも、シェル変数を参照するための$は同様に$$とエスケープしなくてはなりません。

ここでのポイントは、「Makefileに複雑なコマンド列を書くとエスケープが大変」ということです。$をエスケープし忘れて期待しない挙動になることがあります。

個々のコマンド列は別々のシェルで実行

Makefileでは1つのターゲットについて2行以上のビルド用コマンドを記述できますが、それぞれの行は別々のプロセスのシェルで実行されるという事に注意しなくてはいけません。

例えば、先のMakefileの例について、ソースコードを直接ZIPファイルに圧縮するのではなく、一旦作業ディレクトリにコピーして、コメント行を削除してからZIPファイルに圧縮する、ということをしたくなったとしましょう。単純に考えると、以下のように書きたくなるところです。

1
2
3
4
5
6
7
8
9
$(PACKAGE_NAME).jar: $(JAR_TARGET_FILES)
        rm -f $(PACKAGE_NAME).jar
        rm -rf jar_temp
        mkdir -p jar_temp
        cp -r $(JAR_TARGET_FILES) jar_temp/
        cd jar_temp
        find -name *.js | xargs sed -i -r -e "s#^\s*//.*##"
        zip -q -r -9 ../$(PACKAGE_NAME).jar $(JAR_TARGET_FILES) -x \*.git/\*
        cd ..

ですが、これは期待通りに動作しません。各行のコマンドは別々のプロセスのシェルで実行されるため、cdでのカレントディレクトリの移動のようにそのプロセス内でのみ効果があるコマンドは、効果が各行でリセットされてしまいます。そのため、これでは「トップレベルのディレクトリからテンポラリディレクトリにcdした後、そこでfindコマンドを実行する」ではなく、「cdした後すぐにシェルを終了する。次に、トップレベルのディレクトリでfindコマンドを実行する」ということになってしまい、変更されて欲しくないファイルにまで変更が及んでしまいます。

よって、このような場合は一連のコマンドとして実行されて欲しい内容を1行にまとめて記述する必要があります。

1
2
3
4
5
6
$(PACKAGE_NAME).jar: $(JAR_TARGET_FILES)
        rm -f $(PACKAGE_NAME).jar
        rm -rf jar_temp
        mkdir -p jar_temp
        cp -r $(JAR_TARGET_FILES) jar_temp/
        cd jar_temp && find -name *.js | xargs sed -i -r -e "s#^\s*//.*##" && zip -q -r -9 ../$(PACKAGE_NAME).jar $(JAR_TARGET_FILES) -x \*.git/\*

ただ、これでは1行が長すぎるので、このような場合には改行をエスケープして見た目上折り返すのが一般的です。

1
2
3
4
5
6
7
8
$(PACKAGE_NAME).jar: $(JAR_TARGET_FILES)
        rm -f $(PACKAGE_NAME).jar
        rm -rf jar_temp
        mkdir -p jar_temp
        cp -r $(JAR_TARGET_FILES) jar_temp/
        cd jar_temp && \
          find -name *.js | xargs sed -i -r -e "s#^\s*//.*##" && \
          zip -q -r -9 ../$(PACKAGE_NAME).jar $(JAR_TARGET_FILES) -x \*.git/\*

ここでのポイントは、「コマンド列間での情報共有が大変」ということです。カレントディレクトリも変わりませんし、変数を定義しても伝わりません。共有したい場合は;&&などを組み合わせて1行のコマンド列として実行しましょう。一見、欠点のような書き方になっていますが、どのコマンドもキレイな状態で動くので他のコマンドの影響を受けにくいという利点でもあります。

その他、気をつけるべきこと

ビルド用のコマンド列を記述する行は必ずタブ文字でインデントする必要がある、というのも地味ですが重要な点です。Webページ上に記載されたスクリプトをコピー&ペーストすると、タブ文字が連続する半角スペースに変換されてしまうことがあり、そのままMakefileとして使用した際にエラーになってしまいます。

シェルスクリプトではset -eしないとコマンドが失敗しても実行を継続しますが、Makefileではそこで実行が中断します。Makefileでは、失敗してもよいコマンドの場合は最後に|| trueをつけて以下のように書きます。

1
        zip -r ../$(PACKAGE_NAME).jar $(JAR_TARGET_FILES) || true
シェルスクリプトとMakefileはどちらを使えばよいか

以上のように、いくつかの点に気をつければMakefileでも複雑な処理をこなすことはできます。しかしながら、シェルスクリプトとは別の決まりがたくさんあって、それに気をつかいながらコマンドを書いていくということは、ミスを誘発しやすく、また、デバッグもしにくくなります。

シェルの複雑なコマンドとMakefileの単純なルールを組み合わせたために分かりにくくなってしまうのであれば、Makefileの機能をフルに活用すればよいのではないか、ということはいえます。実際に、Makefileには前述した単純なルール以外にも非常に多くの機能があり、シェルで複雑なコマンドを書かなくてもMakefileの機能で代用できる場合もあります。たとえば、FreeBSDのパッケージ管理システムであるFreeBSD Portsはmakeベースのシステムです。

ただ、makeで実現できる高度な機能の多くはmakeの実装毎にMakefileの書き方が異なるため、汎用的に使えるMakefileとするためには注意が必要です。Makefileを生成する仕組みがたくさんあるのはこのためです。GNUのビルドシステム*3もそうですし、imakeやCMake*4などもそうです。

そのため、Makefileだけで頑張るよりは、他のツールを使ったり、他のツールと一緒に活用するのが現実的です。そのときに使えるツールの1つが、makeでも使っているシェルです。先のエントリでは「簡単なビルドスクリプトであればシェルスクリプトで……」といった前置きを書いていましたが、むしろ、Makefileの書き方自体に明るくないのであれば、複雑な処理が必要なビルドスクリプトであればあるほど、シェルスクリプトとして記述した方が簡単です。

Makefileの効果的な利用

ただ、Makefileには、ファイルの依存関係からビルドの順番を自動的に解決するという、シェルスクリプトにはない非常に便利な機能があります。依存関係の解決にだけ着目すれば、覚えなければならないことはそれほどないため、習得はそれほど難しくないでしょう。

よって、以下のように使い分けるのが、シェルスクリプトとMakefileのよいところを互いに引き出しあえて、作るのも管理するのも効率がよくなります。

  • Makefileではビルド対象のファイル同士の依存関係を管理する。
  • ファイルのビルド手順が複雑な場合、シェルスクリプトでビルド手順を記述してしまい、Makefileからは単にそのシェルスクリプトを呼ぶようにする。

また、make以外にもRakeOMakeなど依存関係を解決してくれるツールがあります。makeだけにこだわらず、自分が実現したいことに適したツールを選択するとよいでしょう。

*1 依存するファイルとしてファイルではなくディレクトリも指定できます。ディレクトリ直下のファイルの変更は検出できますが、直下ではなく深い位置のファイルの変更は検出できないので注意してください。個々のファイルを指定する方が安全です。

*2 今はcat version.txtというマクロが定義されていないため空文字列に置き換えられている。

*3 GNU Autoconf/GNU Automake/GNU Libtool

*4 CMakeはMakefile以外にもVisual Studioのslnファイルなども生成できる。

2012-10-24

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|