おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

はじめてのRake(1)

Rakeの初歩について書こうと思います。 1回のブログでは難しいので、4回シリーズで書きます。 その内容はそれぞれ、タスク、ファイルタスク(前半)、ファイルタスク(後半)、応用例です。

文中に[R]という記号で始まる段落は、「Ruby上級者向けの解説」です。 上級とは、ほぼ「クラスを記述できるレベル」を指します。 上級以外の方はこの部分を飛ばしてください。

(2022/7/30追記) Githubに「はじめてのRake」をアップロードしました。 このブログの内容を更に発展させたチュートリアルです。

github.com

Github Pagesもあります。

toshiocp.github.io

Rakeのインストール

RakeはRubyのアプリケーションです。 RubyのインストールについてはRubyの公式ホームページを参考にしてください。 Rakeは「Rubyの標準添付ライブラリ」なので、インストールされたRubyの中に含まれることが多いですが、もしそうでないときは、

  • Linuxディストリビューションrubyパッケージをインストールした場合=>パッケージのrakeをインストール
  • 別の方法でインストールした場合=>コマンドから「gem install rake」でインストール

などの方法でインストールしてください。

Rakeとは?

Rakeは、Makeと同様の機能を、Rubyプログラムとして実装したアプリケーションです。

Makeは、Cでコンパイルするときに、コンパイル過程全体をコントロールするプログラムです。 しかし、MakeはC専用ではなく、いろいろなコンパイラトランスレータ(ある形式から別の形式に変換するプログラム)を制御することができます。 便利なMakeですが、その文法はマニアックです。 初歩的な使い方をしているうちは、分かりやすいのですが、使えば使うほど分かりにくくなっていきます。 例えばこんな感じです。

application: $(OBJS)
    $(CC) -o $(@F) $(OBJS) $(LIBS)

$(OBJS): %.o: %.c $(HEADER)
    $(CC) -fPIC -c -o $(@F) $(CFLAGS) $<

それに対してRakeは

  • Rubyの文法をそのまま使うことができる
  • したがって、分かりやすく柔軟な書き方ができる

という利点があります。

Rakeの基本

まず、コマンドのrakeとカレントディレクトリに置くRakefileというファイルがポイントになります。 rakeコマンドは引数にタスク名(タスクは後で説明します)をとり、

$ rake hello

の形で使います。 この例では、引数helloはタスク名です。

このときrakeは次のことを順に実行します。

  • Rakefileをロード、実行する(Rakefileにはタスクの定義が書かれている)。
  • コマンドライン引数で指定されたタスクを呼び出す(実行する)

Rakefileにタスクの定義を書く」ことが、rakeを使う際のポイントになります。 当然、コマンドラインから呼び出されるタスクはRakefileの中で定義されていなければなりません。

[R] 「タスクの定義を書く」「タスクを定義する」などは、Rubyのドキュメントで使われている言い回しです。 Rubyに熟練している人は、これが具体的に何を意味するか気になるかもしれません。 Ruby的には「Taskクラスのインスタンスを生成する」ことを意味します。

Rakefileでのタスク定義

タスクはオブジェクトで、名前、事前タスク(前提条件)、アクションを持ってますが、事前タスクとアクションは無くても構いません。

それでは、まず名前だけを持っているタスクを作成してみましょう。 Rakefileに次のように書き込みます。

task :simple_task

taskはタスクを定義するためのコマンド(命令)だと考えてください。 一般に「コマンド」はプログラム言語において、コンピュータに何かをさせるためのものです。 例えば、Shellでは、「cd」はカレント・ディレクトリを移動する「コマンド」です。 「cd /var」によって、カレントディレクトリが「/var」に移動しますが、それは「/var引数を与えてcdコマンドを実行した」ことの結果なのです。

同様に「task」コマンドには引数「:simple_task」が与えられています。 そして「taskコマンドを実行することにより、simple_taskを名前とするタスクが作成される」のです。 なお、引数の:simple_taskはシンボルですが、文字列を使っても構いません。

task "simple_task"

両者に対してtaskコマンドが行う動作は全く同じです。

実は、taskコマンドは、Rubyの文法から見ると、taskメソッドの呼び出しで、:simple_taskはtaskメソッドへの引数です。 ですので、今後はtaskを「コマンド」あるいは「メソッド」ということがありますが、

  • 「コマンド」は、taskの「タスク作成」機能に注目している場合
  • 「メソッド」は、Rubyの文法上の機能に注目している場合

で使い分けをしています。 細かいことになるので、あまり気にしなくても構いません。

[R] Rubyの文法から見た場合、taskコマンドは「メソッド呼び出し」で、:simple_taskはtaskメソッドの引数です。 Rubyではメソッド呼び出しの引数にカッコを付けても付けなくても良いのでこのように書けるのです。 もしカッコを付けるのならば、

task("simple_task")

となります。 (taskとカッコの間にはスペースを入れない)。 どちらでも定義できますが、カッコ無しを用いるのが良いです。

「タスクを定義する」とは「Taskクラスのインスタンスを生成する」ことです。 インスタンスの生成には通常newメソッドが使われますが、Task.newよりもtaskメソッドの方が便利です。 なお、taskメソッドでは、その実行の中で「Task.new」が呼び出され、タスクのインスタンスが生成される仕組みになっています。

タスク「simple_task」には事前タスクとアクションは定義されていません。

コマンドラインからタスクを実行してみましょう。

$ rake simple_task

タスクは呼び出されているのですが、アクションが無いため、見た目には何も起こりません。 タスクが定義できているかどうかは、次のようにするとわかります。

$ rake -AT
rake simple_task  #

オプションATは登録されているすべてのタスクを表示します。 これで、simple_taskが定義されていることがわかりました。

アクション

アクションは、taskメソッド呼び出しのブロックで表します。

task :hello do
  print "Hello world!\n"
end

このタスクはhelloという名前です。 helloには事前タスクはありません。 アクションは「Hello world!」と画面表示する、というものです。

では、このタスクを実行してみましょう。

$ rake hello
Hello world!

タスクhelloが呼び出され、そのアクションが実行されて「Hello world!」の文字列が表示されました。

[R] Rubyにはブロックを(1)波カッコ({})で表す(2)doendで表す、の2つの方法があります。 Rakefileではどちらも動作しますが、読みやすさの点からdoendを使うのが良いでしょう。 また、波カッコを使う場合、次のように書くと動作しません。

task :hello {print "Hello world!\n"}

これは、do-endより波カッコの方が強く結合するために起こるエラーです。 Rubyのドキュメントが参考になるので見てください。 これを解消するには、引数にカッコをつけます。

task(:hello) {print "Hello world!\n"}

Rakeでは、「taskがあたかもコマンドであるかのように表現したい」ということがあります。 波カッコを使うとそれができませんから、動作はするけれども推奨はできないのです。

RakeはRuby文法の自由さ(引数のカッコを省略できるなど)を使って、taskなどのコマンドを提供しています。 このように、特定の分野のために作られたコマンドをもつ言語を「DSL(Domain-Specific Language)」といいます。 do-end推奨の背景にはDSLの考え方があります。

事前タスク

あるタスクが事前タスクを持っている場合、そのタスクが呼び出され(実行され)る前に事前タスクを呼び出します。

タスクの定義は

task タスク名 => 事前タスク(の配列)do
  アクション
end

のようになります。

「タスク名=>事前タスク(の配列)」のところは、Rubyのハッシュです。 メソッド呼出の末尾にハッシュを渡す場合は カッコ({}) を省略することができます。 省略しなければ「{タスク名 => 事前タスク(の配列)}」となりますが、これでも動作します。

また、タスク名がシンボルの場合、例えば「:abc => "def"」と書くのを「abc: "def"」と書くことができます。 同様に、「:abc => :def」と「abc: :def」は同じです。

次の例では、firstとsecondという2つのタスクがあり、firstがsecondの事前タスクになっています。

task second: :first do
  print "Second.\n"
end

task :first do
  print "First.\n"
end

タスクsecondを呼び出すと、事前タスクであるfirstがその前に呼び出されます。

firstを実行 => secondを実行

という順になります。

$ rake second
First.
Second.

Rakefileの例

歌川さんの味玉のレシピをMakefileで記述するが面白かったので、そのRake版を作ってみました。

# 味玉をつくる

task :お湯を湧かす do
  print "お湯を湧かします\n"
end

task 卵を茹でる: :お湯を湧かす do
  print "卵を茹でます\n"
end

task :'8分待つ' => :卵を茹でる do
  print "8分待ちます\n"
end

task ボウルに氷を入れる: :'8分待つ' do
  print "ボウルに氷を入れます\n"
end

task ボウルに水を入れる: :ボウルに氷を入れる do
  print "ボウルに水を入れます\n"
end

task ボウルに卵を入れる: :ボウルに水を入れる do
  print "ボウルに卵を入れます\n"
end

task 卵の殻を剥く: :ボウルに卵を入れる do
  print "卵の殻を剥きます\n"
end

task :ジップロックに日付を書く do
  print "ジップロックに日付を書きます\n"
end

task ジップロックにめんつゆを入れる: [:ジップロックに日付を書く, :卵の殻を剥く] do
  print "ジップロックにめんつゆを入れます\n"
end

task ジップロックに卵を入れる: :ジップロックにめんつゆを入れる do
  print "ジップロックに卵を入れます\n"
end

task 一晩寝かせる: :ジップロックに卵を入れる do
  print "一晩寝かせます\n"
end

task 味玉: :一晩寝かせる do
  print "味玉ができました\n"
end

実行してみます。

$ rake 味玉
ジップロックに日付を書きます
お湯を湧かします
卵を茹でます
8分待ちます
ボウルに氷を入れます
ボウルに水を入れます
ボウルに卵を入れます
卵の殻を剥きます
ジップロックにめんつゆを入れます
ジップロックに卵を入れます
一晩寝かせます
味玉ができました

タスクの呼び出しは一度だけ

すでに呼び出されたタスクは実行されません。 つまり「タスクの実行は1度だけ」です。

例えば、味玉のRakefile

task ジップロックに卵を入れる: :ジップロックにめんつゆを入れる do

のところを

task ジップロックに卵を入れる: [:ジップロックにめんつゆを入れる, :卵の殻を剥く] do

とすると、「卵の殻を剥く」が2箇所で事前タスクになります。 呼び出しが2回ありますが、実行は1回だけなので、実行結果は同じになります。

[R] タスクのインスタンス・メソッドに「invoke」(呼び出し)と「execute」(実行)があります。 invokeはアクションを一度だけ実行しますが、executeはそのメソッドが呼ばれた回数だけ何度でも実行します。 それで、Rubyのドキュメントでは「呼び出し」と「実行」の2つの言葉を区別して使っているようです。 このブログでは使い分けが曖昧な箇所がありますが、大きな混乱はないと思っています。 なお、invokeは自身のタスクを呼び出す前に事前タスクを呼び出しますが、executeは事前タスクを呼び出しません。

タスク名には文字列も使える

今までタスク名にシンボルを使ってきましたが、文字列を使うこともできます。

task "simple_task"
task "second" => "first"

このような書き方も可能です。 シンボルでハッシュを記述するには「{abc: :def}」のような書き方ができますが、シンボルの最初に数字がくるときにはこれが使えません。 「{0abc: :def}」や「{abc: :2def}」はシンタックス・エラーになります。 「{:'0abc' => :def}」「{abc: :'2def'}」のように書かなければなりません。 文字列ではこのような心配がなく、シングルあるいはダブルクォートで囲めばエラーにはなりません。

慣例としては

task abc: %w[def ghi]

の書き方が多く用いられるようです。 %wは空白で区切られた文字列の配列を返します。 %w[def ghi]["def", "ghi"]は同じです。 Rubyのドキュメントの%記法を参考にしてください。

味玉の例で%記法を使うと

task ジップロックにめんつゆを入れる: %w[ジップロックに日付を書く 卵の殻を剥く] do

となります。