-> English version



るびくる&RBのRubyプログラミング大作戦!
ファイルを扱う作業をRakeで便利にしよう!(パート2:実践編1)

このエントリーをはてなブックマークに追加

るびくる:
Rubyの自称マスコットキャラクター。
好きなコーヒーは「小岩井ミルクとコーヒー」(キリンビバレッジ)。
次はカフェオレを開拓するか、濃いめのコーヒーにチャレンジするか決めかねている。

RB(あーるびー):
解説役兼るびくるの指導役。
好きなコーヒーはエクセルシオールカフェのラテ系全般。
次はカプチーノに挑戦しようとしているが、独特の飲み口になじめず苦戦中。


ねえねえRB、わたしのイラストどのくらい届いてる?

……へ?

ほら、先月パート1を公開したでしょ?
あのときに多くの人が読んでくれてたみたいだから
読者さんたちが描いてくれた私のイラスト、どんなのが届いてるかなーと思って。

……0枚ですけど。

えええーーー!?

いや、こっちがえええーーーだよ!
何でイラストが何枚も届くと思ったの!?

だって、多くの人が見てくれてたみたいだし
Vジャンプなんかの読者企画では、新連載が始まるやいなやイラストがどんどん届いてるから
てっきり前パートの公開以降、わたし宛のお便りもイラストつきでたくさん届いてるものだとばかり……。

なんでVジャンプを例にあげたのかはよく分からないけど
この連載記事を、Vジャンプの読者と同じくらいの人数が読んでるわけないでしょ?
どういう理論でそんなにすごい勢いでイラストが届くと思ったのか、わけがわからないよ。

イラストはなかったけど、ありがたいことにお便り自体は数通届いてたんだから、それでいいじゃない。

うう……わたし宛のかわいいイラスト……(ぐすぐす)

ほら、元気出して。そろそろパート2始まるよ。

(ぐすぐす)

あ、その前に冷凍パン温めるけど、るびくるも食べる?

わたしミルクパンを3個! それとコーンスープもお願いします!

ちょっと予想はしてたけど、立ち直るのはやっ!


さて、前回でも話したとおり、今回は実用的なRakefileのサンプルとして
HTMLファイルやWebサイトを作るためのRakefileを作ることにしようか。

待ってました! 前回の話では、たしかこういうことができるって話だったよね。

  1. 自動的に、上側のメニューを差し込んだ状態のHTMLファイルを作ってくれる
  2. あるディレクトリに置いた写真の画像ファイル全部を、自動的に縮小してくれる

そう。この2つのうち1番に関しては、簡単なHTMLファイルを作るだけのタスクだから
基本的にはすごく簡単だし、今のるびくるにもできると思うよ。

え? わたし前回の話で依存関係のところまでしか聞いてないんだけど、それでもRakefileが書けるものなの?

うん、基本の書き方と依存関係の考え方さえ分かっていれば、HTMLファイルを作るのには十分だよ。

ただ、「知っているとちょっと便利な細かいテクニック」も、いくつかあるから
そのあたりは、実際のRakefileを見ながら、1つ1つ解説を加えていこうか。


1. 基本的なHTML生成タスクを書いてみよう!

さて、ここからは実際にrakeコマンドを実行してもらうために
Rakeのインストールが必要になるんだけど……
Rake 0.9.2.2(2012年7月22日時点での最新版)のインストールは済んでる?

うん、Rake ユーザガイドインストールページを読んでインストールしておいたから、バッチリだよ!

ただ、この記事のリンク先にあるみたいだから、RubyGemsを使ってRake 0.9.2.2をインストールしたけど
この手順でいいんだよね?

OK。それじゃ、試しにメニューを差し込んでHTMLファイルを作るためのRakefileを書いてみてもらえる?

わかった! 前回での話を参考にすると……
ファイルを作る一連の処理はファイルタスクで定義する、ってことだったよね。
こんな感じでいいのかな?

FileTree

- Rakefile
- template/
    - index.html
    - header_menu.html

file: Rakefile

# encoding: utf-8

desc '全HTMLファイルを作成する'
task :publish => 'index.html'

desc 'トップページのHTMLファイルを作成する'
file 'index.html' => ['template/index.html', 'template/header_menu.html'] do
  # 元になるHTMLファイルの読み込み
  base_html = File.read('template/index.html', encoding: 'utf-8')
  menu_html = File.read('template/header_menu.html', encoding: 'utf-8')
  
  # トップページHTMLの出力
  open('index.html', 'w'){|f|
    f.write base_html.gsub('{HEADER_MENU_HTML}', menu_html)
  }
end

file: template/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>るびくるのグルメサイト「Rubicle Groumet」</title>
</head>

<body>
  {HEADER_MENU_HTML}
  <h1>Rubicle Groumet</h1>
  <p>るびくるのグルメサイトです。おいしい食べ物についての情報を紹介していきます。</p>
</body>
</html>

file: template/header_menu.html

<div id="MENU">
  <ul>
    <li><a href="index.html">トップページ</a></li>
    <li><a href="50tour.html">そば家50店めぐり</a></li>
    <li><a href="no-more-weight.html">太らないための食べ方講座</a></li>
  </ul>
</div>

よし、書けたよ!

なるほど、templateディレクトリ内の index.html と header_menu.html から
サイト公開用の index.html を生成するためのRakefileなんだね。

(しかしサンプル用にしては、サイトの内容が妙に偏ってるなあ……もしかして本番サイト用なのかコレ?)

それじゃこのRakefileを使って、rakeを実行してみるね。

command:

$ rake -T
rake index.html  # トップページのHTMLファイルを作成する
rake publish     # 全HTMLファイルを作成する
$ rake publish
$

FileTree

- index.html
- Rakefile
- template/
    - index.html
    - header_menu.html

file: index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>るびくるのグルメサイト「Rubicle Groumet」</title>
</head>

<body>
  <div id="MENU">
  <ul>
    <li><a href="index.html">トップページ</a></li>
    <li><a href="50tour.html">そば家50店めぐり</a></li>
    <li><a href="no-more-weight.html">太らないための食べ方講座</a></li>
  </ul>
</div>
  <h1>Rubicle Groumet</h1>
  <p>るびくるのグルメサイトです。おいしい食べ物についての情報を紹介していきます。</p>
</body>
</html>

できたー!
メッセージを何も出してないからちょっと味気ないけど、ちゃんとメニューが差し込まれた状態の
index.htmlが生成されたね!

おめでとう!
単純な例だけど、まずはそれができれば、Rakefileの基本的な書き方は大丈夫だよ。

それじゃ次に、このRakefileをもとにして……ん? るびくる、何打ち込んでるの?

$ rake publish
$ rake publish
$ rake publish
$ rake publish
$

……始めてのrake成功でテンション上がったのは分かったから、rakeコマンドの連打はやめて次に行こうよ。


2. よく使う標準的なタスクはdefaultタスクで

それじゃせっかくだし、ここでdefaultタスクの説明もしておこうか。
るびくる、さっきrakeコマンドを連打してたとき
ちょっと思ったことはない?

毎回rake publishって打つのはめんどくさいです。

期待通りの答えをありがとう。

それを敢えて聞いてくるってことは、めんどくさくない方法があるってことだよね!
さあ教えて! 今すぐ教えて! ハリーハリーハリーハリーハリーハリー!

お、おちついて! ハリーの回数多いよ! 

えーっと、rakeコマンドを実行するときに、タスク名を指定しなかった場合には
「default」っていう名前のタスクを自動的に実行してくれるようになってる。
試しにさっきのRakefileに、publishを呼び出すだけのdefaultって名前のタスクを定義してみて。

りょーかい!

file: Rakefile

# encoding: utf-8

task :default => :publish

desc '全HTMLファイルを作成する'
task :publish => 'index.html'

desc 'トップページのHTMLファイルを作成する'
file 'index.html' => ['template/index.html', 'template/header_menu.html'] do
  # 元になるHTMLファイルの読み込み
  base_html = File.read('template/index.html', encoding: 'utf-8')
  menu_html = File.read('template/header_menu.html', encoding: 'utf-8')
  
  # トップページHTMLの出力
  open('index.html', 'w'){|f|
    f.write base_html.gsub('{HEADER_MENU_HTML}', menu_html)
  }
end

command:

$ rake -T
rake index.html  # トップページのHTMLファイルを作成する
rake publish     # 全HTMLファイルを作成する
$ rake
$

ほんとだ! rakeって打ち込むだけでちゃんと実行できてる!

そういうこと。これでるびくるも、さっきみたいに何度もrakeしたいときに
rake publishって打つ必要はなくなったよね?

これでrakeコマンドも打ち放題だね!

……モニターの前の皆さんのために一応言っておくと、普通は2回以上続けて
同じrakeタスクを実行する必要は、まったくないからね。


3. 複数のファイルをRakeしよう

でもここまでだと、まだRakeならではの良いところってのがあんまり出て来てないね。
もうちょっと凝ったRakefileを書きたいところなんだけど……

そうだね。とりあえず、複数ファイルを対象に取るようにしてみたらどう?
実際に使うときには、HTMLファイルが index.html 1つだけってことは、まずありえないだろうし
Rakeが真価を発揮するのは、複数のファイルを扱うときだからね。

そうだね! さっそくHTMLを増やして、Rakefileも書き換えてみるね!

FileTree

- Rakefile
- template/
    - 50tour.html
    - header_menu.html
    - index.html
    - no-more-weight.html

file: Rakefile

# encoding: utf-8

task :default => :publish

desc '全HTMLファイルを作成する'
task :publish => 'index.html'

desc 'トップページのHTMLファイルを作成する'
file 'index.html' => ['template/index.html', 'template/header_menu.html'] do
  # 元になるHTMLファイルの読み込み
  base_html = File.read('template/index.html', encoding: 'utf-8')
  menu_html = File.read('template/header_menu.html', encoding: 'utf-8')
  
  # HTMLの出力
  open('index.html', 'w'){|f|
    f.write base_html.gsub('{HEADER_MENU_HTML}', menu_html)
  }
end

desc '「そば屋50店めぐり」ページのHTMLファイルを作成する'
file '50tour.html' => ['template/50tour.html', 'template/header_menu.html'] do
  # 元になるHTMLファイルの読み込み
  base_html = File.read('template/50tour.html', encoding: 'utf-8')
  menu_html = File.read('template/header_menu.html', encoding: 'utf-8')
  
  # HTMLの出力
  open('50tour.html', 'w'){|f|
    f.write base_html.gsub('{HEADER_MENU_HTML}', menu_html)
  }
end

desc '「太らないための食べ方講座」ページのHTMLファイルを作成する'
file 'no-more-weight.html' => ['template/no-more-weight.html', 'template/header_menu.html'] do
  # 元になるHTMLファイルの読み込み
  base_html = File.read('template/no-more-weight.html', encoding: 'utf-8')
  menu_html = File.read('template/header_menu.html', encoding: 'utf-8')
  
  # HTMLの出力
  open('no-more-weight.html', 'w'){|f|
    f.write base_html.gsub('{HEADER_MENU_HTML}', menu_html)
  }
end

command:

$ rake -D
rake 50tour.html
    「そば屋50店めぐり」ページのHTMLファイルを作成する

rake index.html
    トップページのHTMLファイルを作成する

rake no-more-weight.html
    「太らないための食べ方講座」ページのHTMLファイルを作成する

rake publish
    全HTMLファイルを作成する

$

……勢いで書き足したけど、さすがにこの書き方は安直すぎるね。

そうだね、これだとHTMLファイルを増やしたときに、コピー&ペーストの繰り返しで
Rakefileがどんどん長くなっていっちゃうよね。面倒だし。

せっかくRubyを使ってるんだし、ディレクトリの中にあるHTMLファイルを探して
自動的に欲しいファイルすべてを作ってくれるようにしてみたら?

そうだね。もうちょっと手を加えてみよっか。
あ、ところでRB、Rakefileってメソッド(関数)の定義とかも使えたりするのかな?

もちろん。メソッドどころかクラスだって定義できるよ。
Rakefileは基本的にはRubyスクリプトだから、Rubyスクリプトでできることなら大抵のことはできる。

やった! それならファイルを検索するくらい簡単だね!

えーっと、今回やりたいのは
「templateディレクトリ以下のファイルのうち、メニュー用以外のHTML全部にメニューを差し込む」ことだから
メソッドで、HTMLファイルを検索して……っと。

file: Rakefile

# encoding: utf-8

#-------------------------------------------------------------------------------
# 定数やメソッドの定義
#-------------------------------------------------------------------------------
# 元になるHTMLファイルのリスト(配列)
SOURCE_HTMLS = Dir.glob('template/*.html')
SOURCE_HTMLS.delete('template/header_menu.html') # メニュー用のHTMLは除外

# 出力先HTMLファイルのリスト(配列)をクリア
OUTPUT_HTMLS = []

# 「メニューを差し込んだHTMLを生成する」メソッドを定義
def make_html(source_html_name, output_html_name)
  # 元になるHTMLファイルの読み込み
  source_html = File.read(source_html_name, encoding: 'utf-8')
  menu_html = File.read('template/header_menu.html', encoding: 'utf-8')
    
  # メニューを差し込んだ状態のHTMLファイルを出力
  open(output_html_name, 'w'){|f|
    f.write source_html.gsub('{HEADER_MENU_HTML}', menu_html)
  }

  # 処理内容を出力
  $stderr.puts "#{source_html_name} ---> #{output_html_name}"
end


#-------------------------------------------------------------------------------
# Rakeタスク
#-------------------------------------------------------------------------------
# defaultタスクを定義
task :default => :publish

# 元になるHTMLファイル1つごとにループ
SOURCE_HTMLS.each do |source_html_name|
  # 生成するHTMLのファイル名を決定して、出力先HTMLファイルのリストに追加
  output_html_name = source_html_name.sub(/^template\//, '')
  OUTPUT_HTMLS << output_html_name

  desc "#{output_html_name} を作成する"
  file output_html_name => [source_html_name, 'template/header_menu.html'] do
    make_html(source_html_name, output_html_name)
  end
end

# 出力先HTMLファイルのリスト(配列)をもとにして、publishタスクを定義
desc '全HTMLファイルを作成する'
task :publish => OUTPUT_HTMLS

command:

$ rake -T
rake 50tour.html          # 50tour.html を作成する
rake index.html           # index.html を作成する
rake no-more-weight.html  # no-more-weight.html を作成する
rake publish              # 全HTMLファイルを作成する
$

これでばっちり!
templateディレクトリ内のHTMLファイルを、自動で検索して、Rakeタスクを定義してくれるから
HTMLファイルが4つに増えても5つに増えても対応できるよ!

ついでに処理内容を、コンソールに出力するようにしたから
処理の内容も分かりやすくなったしね。


4. 作成されるファイルと、作成されないファイルの違い(依存関係の解決)

それじゃるびくる、試しにrakeコマンドを実行してみてくれる?

はーい。

command:

$ rake
template/50tour.html ---> 50tour.html
template/index.html ---> index.html
template/no-more-weight.html ---> no-more-weight.html
$

FileTree

- Rakefile
- 50tour.html
- index.html
- no-more-weight.html
- template/
    - 50tour.html
    - header_menu.html
    - index.html
    - no-more-weight.html

できた! ためしにもう1回実行してみよっと。

command:

$ rake
$

あれ、今度は何もタスクが実行されてないよ?

ああ、それは依存してるファイルが更新されてないからだね。

依存してるファイル……?

# 元になるHTMLファイル1つごとにループ
SOURCE_HTMLS.each do |source_html_name|
  # 生成するHTMLのファイル名を決定して、出力先HTMLファイルのリストに追加
  output_html_name = source_html_name.sub(/^template\//, '')
  OUTPUT_HTMLS << output_html_name

  desc "#{output_html_name} を作成する"
  file output_html_name => [source_html_name, 'template/header_menu.html'] do
    make_html(source_html_name, output_html_name)
  end
end

ええと、今回で言えば、50tour.html の元になってるのは

  • template/50tour.html
  • template/header_menu.html

の2ファイルだから……

そう。その元になるファイルのうち、どちらか片方が変更されるまでは
rakeを実行しても 50tour.html は更新されないってこと。

そっか。元になってるファイルが更新されるまでは
一度作ったファイルを、改めて作り直したりはしないってことなんだね。

ということは、作ったファイルを、いったん削除すれば……

command:

$ rm 50tour.html
$ rake
template/50tour.html ---> 50tour.html
$ 

50tour.html だけをもう一回作り直してくれる、ってことなんだ!

お見事! きちんと動きを理解できてるね。
Rakeはじめの一歩としては十分だよ!

やった! それじゃ先生、このRakefileの評価は100点満点で何点!?

50点。

あっれェーーー!?
ここでまさかの持ち上げて落とす!?


冒頭でいきなりショックを受けて、立ち直れなくなったわたしに
さらにこんな仕打ちを……ひどいよRB……

違う違う!
Rubyスクリプトとして見れば良くできてるけど、まだRakeっぽい書き方じゃないってこと!

っていうか「立ち直れなくなった」って、冒頭ではあっという間に立ち直ってたよね!?

Rakeっぽい書き方?

そう。Rakeでできるのは、ただシンプルなタスクを定義・実行することだけじゃないんだよ。
ファイルリスト、ディレクトリタスク、cleanタスク、pathmap、rule、名前空間……
Rakeならではの機能を使うことで、ファイルを扱うスクリプトを、より簡単に書くことができるんだ。

次回「実践編2」では、そうした機能を使って、今のRakefileをよりすっきりしたRakefileにしていくよ!

次回の記事で、わたしもついにRakeの匠になれるんだね!

劇的ビフォーアフターとは、また中途半端に渋いネタを……

パート3:実践編2に続きます。

このエントリーをはてなブックマークに追加

参考Webページ:
rakeライブラリ - Rubyリファレンスマニュアル
Rake - idesaku web (後藤大輔さんによる解説記事)
Rake ユーザガイド (京橋さんによる公式ユーザガイドの日本語訳)
注意事項
  • 本記事の内容は、Rake 0.9.2.2 時点での動作を元にしています。
  • 本記事内で使用しているアイコン画像は、桜去ほとりさんが制作されたものです。クリエイティブ・コモンズ 表示-非営利 3.0ライセンスのもとで、再配布や変更などを行っていただくことができます。
  • 本記事内の文章やソースコードは、CC0ライセンスのもとで、ご自由に利用していただくことができます。
  • ツンツク・モモコのお買い物大作戦と形式が類似していますが、本記事側が一方的に参考にさせていただいただけであり、とくに関連性はありません。

一言メッセージフォーム

るびくるへの質問や、やってほしい企画のリクエスト、Webサイト管理スタッフへの感想・意見・質問など、なんでもお気軽にどうぞ。