Rユーザーの生産性を高める

Rユーザーのためのmake入門

本稿では「Rユーザーのためのmake入門」と題して、makeというビルドツールを紹介します。makeはRに限らずさまざまなプロジェクトで汎用的に使われており、使い慣れておくと日常のコマンド実行を効率化できるかもしれません。

今回はMakefileの基本的な書き方から、複数のRmdファイルをmakeを使って一括で変換する例を紹介します。締切直前に複数のRmdファイルを編集していたとしても、確実にすべてのレポートをコマンド1発で最新にできるようになるでしょう。

makeはすでにお使いの環境にインストール済みかもしれませんし、インストールされていなかったとしても、パッケージマネージャなどで簡単にインストールできるでしょう。本稿では再現性のため、Docker上でRStudioやmakeを動かします。動作確認はrocker/tidyverse:4.2.1で行っています。

以下を実行して、RStudio Serverを起動し、ブラウザでhttp://localhost:8787を開き、Username:rstudio, Password:foobarでログインしてください。

docker run --rm -p 8787:8787 -e PASSWORD=foobar rocker/tidyverse:4.2.1

makeしてみよう

RStudioを使っていれば、通常はKnitボタンを押してRmdファイルを変換するでしょう。同じことはR Consoleからrmarkdown::renderを呼び出すことでも行えます。

rmarkdown::render("example.Rmd")

また、以下のコマンドでターミナルから別のRのプロセスを起動してRmdファイルを変換することもできます。

Rscript -e 'rmarkdown::render("example.Rmd")'

Makefileには以下のように変換のルールを指定します。

target: dependencies ...
    commands
  • target:作成したいターゲット
  • dependencies:ターゲットが依存するもの、つまりdependenciesを元にターゲットが作られる
  • commands:作成するためのコマンド

今回の例ではRmdファイルをもとにhtmlファイルを生成したいので、以下のように記述します。Makefileと名前を付けて保存してください。

example.html: example.Rmd
    Rscript -e 'rmarkdown::render("example.Rmd")'

コマンドの先頭、つまりRscriptから始まる行の先頭にはタブ文字を入力します。スペースではなくタブです。RStudioではMakefileという名前のファイルでTabを押すとタブ文字が入力されるはずです。タブ文字が正しく入力されているかを確認するには、Global Optionsの「Code⁠⁠→⁠Display」にある「Show whitespace characters」にチェックを入れてください。図1のようにタブ文字がグレーの横棒で可視化されるようになります。

図1 空白文字を可視化
図1

makeを実行してみましょう。Terminalタブでmake <ターゲット>を実行すると、makeはMakeFileを読み込み、指定されたターゲットを生成します。

make example.html
Rscript -e 'rmarkdown::render("example.Rmd")'

Rmdファイルを変換するコマンドが実行され、ターゲットであるhtmlファイルが出力されました。

ちなみにターゲットが指定されていない場合、Makefileで最初に定義されたターゲットを生成します。つまりこの場合は単にmakeを実行しても大丈夫です。

make

2回続けてmakeを実行するとどうなるでしょう。

make
make: 'example.html' is up to date.

makeは「example.htmlは最新です」と表示し、何もしません。makeはターゲットが古くなっているとき、つまりターゲットよりも元となるファイルが新しいときのみ変換コマンドを実行するためです。元となるファイル、example.Rmdを編集した後makeを実行すると、makeは再びコマンドを実行してRmdファイルをhtmlファイルに変換します。

このように、makeは何を編集したら、何を実行すべきかを判断して実行してくれるツールです。

パターンルール

ターゲットが1つだけの単純な例では、makeの恩恵を感じられないかもしれません。もう少し複雑な例を紹介します。

複数のRmdファイルを変換したいケースを考えましょう。もちろん以下のように変換したいRmdファイルの数だけルールを列挙することはできます。

example1.html: example1.Rmd
    Rscript -e 'rmarkdown::render("example1.Rmd")'

example2.html: example2.Rmd
    Rscript -e 'rmarkdown::render("example2.Rmd")'

しかし、これはほぼ同じ内容の繰り返しで冗長で分かりづらく、書き間違える可能性もあります。また、コマンド部分を修正したい場合、すべての該当箇所を書き換える必要があります。

抽象化しましょう。makeにはパターンルールというしくみがあります。パターンルールはターゲットの指定に%を含むことができ、%は空ではない任意の文字列にマッチします。つまり%.html: %.Rmdのように記述でき、素直に「Rmdファイルからhtmlファイルを生成する」と読めるでしょう。パターンルールを使うと、任意のRmdファイルの変換を以下のように指定できます。

%.html: %.Rmd
    Rscript -e 'rmarkdown::render("$<")'

$<という見慣れない記号が現れました。これは自動変数と呼ばれるもののひとつで、ソースファイルの名前に置換されます[1]。つまりmake example1.htmlを実行したとき、$<example1.Rmdに置換され、結果コマンドRscript -e 'rmarkdown::render("example1.Rmd")'が実行されます。

パターンルールによって、任意の名前のRmdファイルが変換できるようになりました。

make example1.html
make example2.html

より実用的に

これまでの例では、ターゲットは作成したいファイル名を指していました。makeでは擬似ターゲットという、実存しない抽象的なターゲットを定義できます。makeには.PHONYという特殊なターゲットが用意されていて、.PHONYの依存項目に指定したものを疑似ターゲットに定義できます。例を挙げます。

.PHONY: html
html: example1.html example2.html

ここでhtmlは疑似ターゲットで、htmlをターゲットに指定すると、依存項目であるexample1.htmlexample2.htmlを生成しようとします。Makefile全体は以下のようになります。

.PHONY: html
html: example1.html example2.html

%.html: %.Rmd
    Rscript -e 'rmarkdown::render("$<")'

これでmake html、または単にmakeを実行するだけで、変更のあったRmdファイルだけを変換できるようになりました。

なお、よく使われる疑似ターゲットにcleanがあります。cleanは通常はmakeで生成したファイルをすべて削除して、makeを実行前の状態に戻すために使われます。今回の例では以下のように指定するとよいでしょう。

.PHONY: clean
clean:
    rm -f *.html

このルールによって、make cleanでhtmlファイルすべてを削除できるようになります。htmlターゲットと違い、cleanには依存項目を指定していないため、make cleanを実行するとコマンドrm -f *.htmlは必ず実行されます。

ここまで使いこなせると、Makefileがずいぶん実用的になります。make html、または単にmakeを実行すれば常に成果物となるhtmlファイルは最新になります。複数のRmdファイルを更新してうっかりそのうちの1つを変換し忘れる、ということもなくなるでしょう。Makefileを書いたおかげで、何を生成するためにどんなコマンドを実行するべきかを覚えておく必要がなくなりました。

あともう少し工夫して、Makefileをより汎用的にしましょう。まずは変数です。他のプログラミング言語同様、makeは変数を扱うことができます。値に適切な名前を付けることで、それが何であるかを把握しやすくなります。先ほどのhtmlターゲットを変数を使って書き換えてみましょう。

htmls = example1.html example2.html

.PHONY: html
html: $(htmls)

変数名を$()で囲うことで値を取り出すことができます。

ところで、htmlファイルをひとつひとつ列挙するのは大変ですし、対象となるファイルが増えるたびにMakefileを修正するのも面倒です。ワイルドカードを使って、*.htmlのようにすべてのhtmlファイルを指定したいですよね。変数の値にワイルドカードで指定したファイル名を展開するには、wildcard関数を使います。

htmls = $(wildcard *.html)

実はこの指定方法では十分ではありません。まだ一度もmakeを実行していない場合やmake cleanを実行した直後はワイルドカードにマッチするhtmlファイルが存在せず、htmlsターゲットが変換する対象が空になってしまい、何も実行されないからです。まずはソースとなるRmdファイルを列挙し、その結果を元にターゲットのhtmlファイルのリストを作りましょう。以下のように指定します。

rmds = $(wildcard *.Rmd)
htmls = $(rmds:.Rmd=.html)

$(rmds:.Rmd=.html)置換参照という機能を使って、rmdsの値の中の.Rmdをすべて.htmlに置き換えています。

これで任意のRmdファイルをhtmlに変換できるようになりました。Makefile全体は以下のようになります。

rmds = $(wildcard *.Rmd)
htmls = $(rmds:.Rmd=.html)

.PHONY: html
html: $(htmls)

%.html: %.Rmd
    Rscript -e 'rmarkdown::render("$<")'

.PHONY: clean
clean:
    rm -f *.html

targetsとの使い分け

「#R登山本」の呼称で好評いただいているRが生産性を高めるの第7章で紹介したtargetsパッケージもまた、処理同士の依存関係を管理して実行できます。実現したい内容によって使い分けると良いでしょう。例えば各タスクがファイルを入出力(ファイルを読み込んで別のファイルを書き出す)する場合は、汎用的なmakeを使うことができます。逆にRを実行中にRのオブジェクトの変更を検知したいのであれば、targetsパッケージを使うべきでしょう。

まとめ

本稿では、Makefileの基本的な書き方、そして実用的なMakefileを書くためのいくつかのしくみを説明しました。makeは一度書けば、欲しいもののために何をすべきかを判断して実行してくれる、動く手順書と言えるでしょう。

makeはとても多機能なツールで、この記事で紹介し切れなかった機能がたくさんあります。より使いこなしたい場合は公式のマニュアルを参照してください。

では良いコマンドラインライフを。

おすすめ記事

記事・ニュース一覧